<?php

declare(strict_types=1);

namespace Xentral\Modules\Pipedrive\Service;

use Xentral\Modules\Pipedrive\Exception\PipedriveConfigurationException;
use Xentral\Modules\Pipedrive\Exception\PipedriveMetaException;
use Xentral\Modules\Pipedrive\Gateway\PipedriveDealGateway;
use Xentral\Modules\Pipedrive\Gateway\PipedrivePersonPropertyGateway;
use Xentral\Modules\Pipedrive\Wrapper\PipedriveAddAddressRoleWrapper;
use Xentral\Modules\SystemConfig\SystemConfigModule;

final class PipedriveConfigurationService
{
    /** @var string */
    private const PIPEDRIVE_SETTINGS = 'pipedrive_settings';

    /** @var string */
    private const PIPEDRIVE_CONF_NAME = 'pipedrive_conf.json';

    /** @var array $_defaultSettings */
    private static $_defaultSettings = [
        'pd_sync_deals'     => true,
        'pd_sync_addresses' => true,
        'pd_api_key'        => null,
    ];

    /** @var SystemConfigModule $configWrapper */
    private $configWrapper;

    /** @var PipedriveMetaWriterService $metaWriterService */
    private $metaWriterService;

    /** @var PipedrivePersonPropertyGateway $propertyGateway */
    private $propertyGateway;

    /** @var PipedriveDealGateway $pipedriveDealGateway */
    private $pipedriveDealGateway;

    /** @var PipedriveMetaReaderService $metaReaderService */
    private $metaReaderService;

    /** @var PipedriveAddAddressRoleWrapper $addAddressRoleWrapper */
    private $addAddressRoleWrapper;

    /**
     * @param SystemConfigModule             $configWrapper
     * @param PipedriveMetaWriterService     $metaWriterService
     * @param PipedrivePersonPropertyGateway $propertyGateway
     * @param PipedriveDealGateway           $pipedriveDealGateway
     * @param PipedriveMetaReaderService     $metaReaderService
     * @param PipedriveAddAddressRoleWrapper $addAddressRoleWrapper
     */
    public function __construct(
        SystemConfigModule $configWrapper,
        PipedriveMetaWriterService $metaWriterService,
        PipedrivePersonPropertyGateway $propertyGateway,
        PipedriveDealGateway $pipedriveDealGateway,
        PipedriveMetaReaderService $metaReaderService,
        PipedriveAddAddressRoleWrapper $addAddressRoleWrapper
    ) {
        $this->configWrapper = $configWrapper;
        $this->metaWriterService = $metaWriterService;
        $this->propertyGateway = $propertyGateway;
        $this->pipedriveDealGateway = $pipedriveDealGateway;
        $this->metaReaderService = $metaReaderService;
        $this->addAddressRoleWrapper = $addAddressRoleWrapper;
    }

    /**
     * @param string $name
     * @param string $value
     *
     * @throws PipedriveConfigurationException
     *
     * @return void
     */
    public function trySetConfiguration(string $name, string $value): void
    {
        if (empty($name)) {
            throw new PipedriveConfigurationException('Cannot set Configuration');
        }

        $this->configWrapper->setValue(self::PIPEDRIVE_SETTINGS, $name, $value);
    }

    /**
     * @param string $name
     *
     * @throws PipedriveConfigurationException
     *
     * @return string|null
     */
    public function tryGetConfiguration(string $name): ?string
    {
        if (empty($name)) {
            throw new PipedriveConfigurationException('Cannot get configuration on Empty');
        }

        return $this->configWrapper->tryGetValue(self::PIPEDRIVE_SETTINGS, $name);
    }

    /**
     * @param array  $settings
     * @param string $value
     *
     * @throws PipedriveConfigurationException
     * @throws PipedriveMetaException
     *
     * @return array
     */
    public function getEncryptedConfiguration(array $settings, string $value): array
    {
        if (empty($settings)) {
            throw new PipedriveConfigurationException('Cannot set Configuration');
        }
        $encValue = $this->encrypt($value);
        $settings['pd_api_key'] = $encValue;

        return $settings;
    }

    /**
     * @throws PipedriveConfigurationException
     * @throws PipedriveMetaException
     *
     * @return string|null
     */
    public function getDecryptedConfiguration(): ?string
    {
        $settings = $this->getSettings();

        return $settings['pd_api_key'] ?? null;
    }

    /**
     * @param string $plaintext
     * @param string $sCipher
     *
     * @throws PipedriveConfigurationException
     * @throws PipedriveMetaException
     *
     * @return string
     */
    protected function encrypt(string $plaintext, string $sCipher = 'aes-128-gcm'): string
    {
        if (!in_array($sCipher, openssl_get_cipher_methods(), true)) {
            throw new PipedriveConfigurationException(sprintf('Cipher method %s does not exist', $sCipher));
        }
        if (null === $this->getNonceSalt()) {
            return $plaintext;
        }

        $key = hash('sha256', $this->getNonceSalt());
        $ivLen = openssl_cipher_iv_length($sCipher);
        $iv = openssl_random_pseudo_bytes($ivLen, $crypto_strong);

        if ($iv === false || $crypto_strong === false) {
            throw new PipedriveConfigurationException('Bad Random length');
        }
        $cipherTextRaw = openssl_encrypt($plaintext, $sCipher, $key, $options = 0, $iv, $tag);

        return base64_encode($iv . $cipherTextRaw . '..' . $tag);
    }

    /**
     * @param string $string
     * @param string $sCipher
     *
     * @throws PipedriveMetaException
     *
     * @return false|string|null
     */
    protected function decrypt(string $string, string $sCipher = 'aes-128-gcm')
    {
        if (empty($string) || !$this->isBase64Encoded($string)) {
            return '';
        }
        if (null === $this->getNonceSalt()) {
            return $this->isBase64Encoded($string) ? null : $string;
        }
        $stringDecode = base64_decode($string);
        $encExploded = explode('..', $stringDecode);
        $enc = array_shift($encExploded);
        $tag = implode('', $encExploded);
        $key = hash('sha256', $this->getNonceSalt());
        $ivLen = openssl_cipher_iv_length($sCipher);
        $iv = substr($enc, 0, $ivLen);
        $cipherTextRaw = substr($enc, $ivLen);

        return openssl_decrypt($cipherTextRaw, $sCipher, $key, $options = 0, $iv, $tag);
    }

    /**
     * @param string $string
     *
     * @return bool
     */
    private function isBase64Encoded(string $string): bool
    {
        return base64_encode(base64_decode($string)) === $string;
    }

    /**
     * @return false|string|null
     */
    private function generateSecureSalt()
    {
        $rand = sprintf('%s', mt_rand());

        return password_hash(uniqid($rand, true), PASSWORD_BCRYPT);
    }

    /**
     * @param bool $force
     *
     * @throws PipedriveMetaException
     *
     * @return false|int
     */
    public function createSalt(bool $force = false)
    {
        if ($force === true) {
            $this->metaWriterService->delete(self::PIPEDRIVE_CONF_NAME);
        }
        if ($this->metaReaderService->exists(self::PIPEDRIVE_CONF_NAME) && $this->metaReaderService->hasKey(
                'nonce_salt',
                self::PIPEDRIVE_CONF_NAME
            )) {
            return -1;
        }

        return $this->metaWriterService->save(self::PIPEDRIVE_CONF_NAME, ['nonce_salt' => $this->generateSecureSalt()]);
    }

    /**
     * @throws PipedriveMetaException
     *
     * @return string|null
     */
    private function getNonceSalt(): ?string
    {
        $data = $this->metaReaderService->readFromFile(self::PIPEDRIVE_CONF_NAME);

        return $data['nonce_salt'] ?? null;
    }

    /**
     * @param array $hContact
     *
     * @throws PipedriveConfigurationException
     *
     * @return array
     */
    public function formatAddressByResponse(array $hContact): array
    {
        if (empty($hContact)) {
            throw new PipedriveConfigurationException('Invalid contact');
        }

        $leadFields = $this->matchSelectedAddressFreeField();
        $lsField = $leadFields['pipedrive_ls_field'];

        $ahEmail = array_map(
            static function ($email) {
                if (!array_key_exists('primary', $email) && $email['primary'] === true) {
                    return null;
                }

                return $email;
            },
            $hContact['email']
        );
        $email = array_filter($ahEmail);
        $primaryEmail = is_array($email[0]) && array_key_exists('value', $email[0]) ? $email[0]['value'] : '';

        $ahPhone = array_map(
            static function ($phone) {
                if (!array_key_exists('primary', $phone) && $phone['primary'] === true) {
                    return null;
                }

                return $phone;
            },
            $hContact['phone']
        );

        $phone = array_filter($ahPhone);
        $primaryPhone = is_array($phone[0]) && array_key_exists('value', $phone[0]) ? $phone[0]['value'] : '';

        return [
            'lead'            => 1,
            'typ'             => !empty($hContact['org_name']) ? 'firma' : 'herr',
            'sprache'         => 'deutsch',
            'name'            => $hContact['org_name'] ?? $hContact['name'],
            'vorname'         => empty($hContact['first_name']) ? 'Pipedrive - ' : $hContact['first_name'],
            'nachname'        => empty($hContact['last_name']) ? $primaryEmail : $hContact['last_name'],
            'land'            => empty($hContact['country']) ? 'DE' : $hContact['country'],
            'telefon'         => $primaryPhone ?? '',
            'email'           => $primaryEmail,
            'kundenfreigabe'  => 1,
            'waehrung'        => 'EUR',
            'ansprechpartner' => !empty($hContact['org_name']) ? $hContact['name'] : '',
            $lsField          => empty($hContact['label']) ? '' : $hContact['label'],
        ];
    }

    /**
     * @throws PipedriveConfigurationException
     *
     * @return array
     */
    public function matchSelectedAddressFreeField(): array
    {
        $hFields = [];
        $asAddressFreeFieldValues = $this->propertyGateway->getConfiguredFreeAddressFieldValues();
        $pdConfFields = [
            'pipedrive_ls_field' => $this->tryGetConfiguration('pipedrive_ls_field'),
        ];

        foreach ($asAddressFreeFieldValues as $fieldName) {
            $addrField = 'adresse' . $fieldName;
            if (in_array($addrField, $pdConfFields, true)) {
                $indexField = array_search($addrField, $pdConfFields, true);
                $hFields[$indexField] = $fieldName;
            }
        }

        if (empty($hFields)) {
            throw new PipedriveConfigurationException('Pipedrive Label-Status fields cannot be matched');
        }

        return $hFields;
    }

    /**
     * @param array $address
     *
     * @throws PipedriveConfigurationException
     *
     * @return array
     */
    public function formatAddressToPipedriveContact(array $address): array
    {
        if (empty($address)) {
            throw new PipedriveConfigurationException('Address is invalid');
        }

        $leadFields = $this->matchSelectedAddressFreeField();
        $lsField = $leadFields['pipedrive_ls_field'];

        return [
            'email'      => $address['email'],
            'first_name' => empty($address['vorname']) ? $address['name'] : $address['vorname'],
            'last_name'  => empty($address['nachname']) ? $address['name'] : $address['nachname'],
            'name'       => $address['name'],
            'phone'      => $address['telefon'],
            'label'      => $address[$lsField],
        ];
    }

    /**
     * @param array $hDeal
     *
     * @throws PipedriveConfigurationException
     *
     * @return array
     */
    public function formatDealToInternal(array $hDeal): array
    {
        if (empty($hDeal)) {
            throw new PipedriveConfigurationException('Error! Deal cannot be formatted for Xentral');
        }

        $dealStage = $this->propertyGateway->getMappingByValueAndType($hDeal['stage_id'], 'deals');
        $asAddTime = [];
        $addTime = $hDeal['add_time'] ?? null;
        if ($addTime !== null) {
            $asAddTime = explode(' ', $addTime);
        }

        return [
            'bezeichnung'      => $hDeal['title'],
            'datum_angelegt'   => !empty($asAddTime) ? $asAddTime[0] : null,
            'zeit_angelegt'    => !empty($asAddTime) ? $asAddTime[1] : null,
            'datum_erinnerung' => null,
            'zeit_erinnerung'  => null,
            'betrag'           => array_key_exists('value', $hDeal) ? (float)$hDeal['value'] : 0.00,
            'stages'           => !empty($dealStage['wiedervorlage_stage_id']) ?
                $dealStage['wiedervorlage_stage_id'] : 0,
            'chance'           => !empty($hDeal['probability']) ? $hDeal['probability'] : 0,
        ];
    }

    /**
     * @param array $resubmission
     *
     * @throws PipedriveConfigurationException
     *
     * @return array
     */
    public function formatResubmissionToPipedriveDeal(array $resubmission): array
    {
        if (empty($resubmission)) {
            throw new PipedriveConfigurationException('Resubmission is invalid');
        }

        $status = 'open';
        if (!empty($resubmission['abgeschlossen'])) {
            if ($resubmission['status'] === 'gewonnen') {
                $status = 'won';
            } elseif ($resubmission['status'] === 'verloren') {
                $status = 'lost';
            }
        }

        $mapping = $this->pipedriveDealGateway->getMappingStageByResubmissionStageId($resubmission['stages']);

        return [
            'title'       => $resubmission['bezeichnung'],
            'stage_id'    => !empty($mapping) ? $mapping['value'] : null,
            'value'       => empty($resubmission['betrag']) ? 0.00 : $resubmission['betrag'],
            'probability' => $resubmission['chance'],
            'status'      => $status,
        ];
    }

    /**
     * @param array $settings
     *
     * @throws PipedriveConfigurationException
     */
    public function setSettings($settings = []): void
    {
        $this->trySetConfiguration(
            self::PIPEDRIVE_SETTINGS,
            json_encode($settings, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)
        );
    }

    /**
     * @throws PipedriveConfigurationException
     * @throws PipedriveMetaException
     *
     * @return array
     */
    public function getSettings(): array
    {
        $settingsRaw = $this->tryGetConfiguration(self::PIPEDRIVE_SETTINGS);
        if (empty($settingsRaw)) {
            return static::$_defaultSettings;
        }

        $settings = json_decode($settingsRaw, true);
        if ($settings === null && json_last_error() === JSON_ERROR_NONE) {
            throw new PipedriveConfigurationException(json_last_error_msg());
        }

        if (empty($settings)) {
            return static::$_defaultSettings;
        }

        if (array_key_exists('pd_api_key', $settings) && !empty($settings['pd_api_key'])) {
            $settings['pd_api_key'] = $this->decrypt($settings['pd_api_key']);
        }

        return $settings;
    }

    /**
     * @param int $contactId
     *
     * @throws PipedriveConfigurationException
     * @throws PipedriveMetaException
     *
     * @return void
     */
    public function addContactToGroup(int $contactId = 0): void
    {
        $defaultSettings = $this->getSettings();
        $contactGrpId = array_key_exists('pd_contact_grp', $defaultSettings) ? $defaultSettings['pd_contact_grp'] : 0;
        if (!empty($contactGrpId)) {
            $this->addAddressRoleWrapper->add($contactId, $contactGrpId);
        }
    }

}