<?php
/**
 * Magento 2 Google Analytics 4 (GA4) GTM
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Anowave license that is
 * available through the world-wide-web at this URL:
 * https://www.anowave.com/license-agreement/
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade this extension to newer
 * version in the future.
 *
 * @category 	Anowave
 * @package 	Anowave_Ec
 * @copyright 	Copyright (c) 2026 Anowave (https://www.anowave.com/)
 * @license  	https://www.anowave.com/license-agreement/
 */

namespace Anowave\Ec\Model;

use Exception;
use GuzzleHttp\Exception\RequestException;

use Google\Ads\GoogleAds\Lib\V20\GoogleAdsClient;
use Google\Ads\GoogleAds\Lib\V20\GoogleAdsClientBuilder;
use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder;
use Google\Ads\GoogleAds\V20\Enums\ConversionActionCategoryEnum\ConversionActionCategory;
use Google\Ads\GoogleAds\V20\Enums\ConversionActionStatusEnum\ConversionActionStatus;
use Google\Ads\GoogleAds\V20\Enums\ConversionActionTypeEnum\ConversionActionType;
use Google\Ads\GoogleAds\V20\Enums\ConversionAdjustmentTypeEnum\ConversionAdjustmentType;
use Google\Ads\GoogleAds\V20\Enums\UserIdentifierSourceEnum\UserIdentifierSource;
use Google\Ads\GoogleAds\V20\Resources\ConversionAction;
use Google\Ads\GoogleAds\V20\Resources\ConversionAction\ValueSettings;
use Google\Ads\GoogleAds\V20\Services\ConversionActionOperation;
use Google\Ads\GoogleAds\V20\Services\MutateConversionActionsRequest;
use Google\Ads\GoogleAds\V20\Services\GclidDateTimePair;
use Google\Ads\GoogleAds\V20\Services\UploadConversionAdjustmentsRequest;
use Google\Ads\GoogleAds\V20\Services\ConversionAdjustment;
use Google\Ads\GoogleAds\V20\Services\RestatementValue;
use Google\Ads\GoogleAds\V20\Services\SearchGoogleAdsStreamRequest;
use Google\Ads\GoogleAds\V20\Common\OfflineUserAddressInfo;
use Google\Ads\GoogleAds\V20\Common\UserIdentifier;
use Google\Ads\GoogleAds\V20\Common\Consent;
use Google\Ads\GoogleAds\Util\V20\ResourceNames;
use Google\ApiCore\ApiException;
use Google\Ads\GoogleAds\Examples\Utils\Helper;
use Google\Ads\GoogleAds\Util\V20\GoogleAdsFailures;
use Google\Ads\GoogleAds\V20\Common\ItemAttribute;
use Google\Ads\GoogleAds\V20\Common\StoreSalesMetadata;
use Google\Ads\GoogleAds\V20\Common\TransactionAttribute;
use Google\Ads\GoogleAds\V20\Common\UserData;
use Google\Ads\GoogleAds\V20\Enums\OfflineUserDataJobTypeEnum\OfflineUserDataJobType;
use Google\Ads\GoogleAds\V20\Resources\OfflineUserDataJob;
use Google\Ads\GoogleAds\V20\Services\AddOfflineUserDataJobOperationsRequest;
use Google\Ads\GoogleAds\V20\Services\Client\OfflineUserDataJobServiceClient;
use Google\Ads\GoogleAds\V20\Services\CreateOfflineUserDataJobRequest;
use Google\Ads\GoogleAds\V20\Services\OfflineUserDataJobOperation;
use Magento\Sales\Api\OrderRepositoryInterface;
use Anowave\Ec\Model\LogFactory;
use Anowave\Ec\Model\LogRepository;
use Anowave\Ec\Helper\Data;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;

class Ads
{
    const DATE_FORMAT = 'Y-m-d h:i:sP';

    private $customer_id;

    /**
     * @var Data
     */
    protected $helper;

    /**
     * @var LogFactory
     */
    protected $logFactory;

    /**
     * @var LogRepository
     */
    protected $logRepository;

    /**
     * @var OrderRepositoryInterface
     */
    protected $orderRepositoryInterface;

    /**
     * @var DateTime
     */
    protected $dateTime;

    protected $timezoneInterface;

    /**
     * @var GoogleAdsClient
     */
    private $client = null;
    
    /**
     * Execution errors
     */
    private $errors = [];

    /**
     * @param Data
     */
    public function __construct
    (
        Data $helper,
        LogFactory $logFactory,
        LogRepository $logRepository,
        OrderRepositoryInterface $orderRepositoryInterface,
        DateTime $dateTime,
        TimezoneInterface $timezoneInterface
    )
    {
        $this->helper = $helper;
        $this->logFactory = $logFactory;
        $this->logRepository = $logRepository;
        $this->dateTime = $dateTime;
        $this->timezoneInterface = $timezoneInterface;

        $this->orderRepositoryInterface = $orderRepositoryInterface;

        if ($this->helper->getJsonKey())
        {
            putenv("GOOGLE_APPLICATION_CREDENTIALS={$this->helper->getJsonKey()}");
        }

        $this->customer_id = $this->formatCustomerId
        (
            $this->helper->getGoogleAdsApiCustomerId()
        );
    }

    /**
     * Offline conversion
     */
    public function offline(?string $orderId)
    {
        return true;
    }


    /**
     * Apply adjustment 
     * 
     * @param array $adjustment
     * 
     * @return [type]
     */
    public function applyAdjustment(int $orderId = 0, array $adjustment = []) : bool
    {
        return $this->adjust($orderId, $adjustment);
    }

    /**
     * Apply adjustment 
     * 
     * @param array $adjustment
     * 
     * @return [type]
     */
    public function applyEnhancement(int $orderId = 0, array $enhancement = []) : bool
    {
        return $this->enhance($orderId, $enhancement);
    }

    /**
     * Adjust conversion
     * 
     * @param int $orderId
     * @param array $adjustment
     * 
     * @return [type]
     */
    public function adjust(int $orderId = 0, array $adjustment = []) : bool
    {
        $this->errors = [];

        $order = null;

        try 
        {
             /**
             * Load order
             * 
             * @var \Magento\Sales\Model\Order
             */
            $order = $this->orderRepositoryInterface->get($orderId);
        }
        catch (Exception $e)
        {
            $this->log(sprintf(__('Order %s cannot be loaded. Failed to upload conversion adjustment'), $orderId));

            return true;
        }

        $adjustmentDateTime = date(static::DATE_FORMAT);


        $conversionActionId = $this->helper->getGoogleAdsApiConversionActionId();

        $conversionAdjustmentType = (int) $adjustment['conversionType'];


        // Applies the conversion adjustment to the existing conversion.
        $conversionAdjustment = new ConversionAdjustment
        (
            [
                'conversion_action'     => ResourceNames::forConversionAction($this->customer_id, $conversionActionId),
                'adjustment_type'       => $conversionAdjustmentType,
                'order_id'              => $order->getIncrementId(),
                'adjustment_date_time'  => $adjustmentDateTime
            ]
        );

        $restatementValue = (float) $adjustment['conversionValue'];

        /**
         * Sets adjusted value for adjustment type RESTATEMENT.
         */
        if ($restatementValue !== null && $conversionAdjustmentType === ConversionAdjustmentType::RESTATEMENT) 
        {
            $conversionAdjustment->setRestatementValue(new RestatementValue(
            [
                'adjusted_value' => $restatementValue
            ]));
        }

        /**
         * @var Issues a request to upload the conversion adjustment
         */
        $conversionAdjustmentUploadServiceClient = $this->getClient()->getConversionAdjustmentUploadServiceClient();


        $response = $conversionAdjustmentUploadServiceClient->uploadConversionAdjustments
        (
            UploadConversionAdjustmentsRequest::build($this->customer_id, [$conversionAdjustment], true)
        );


        if ($response->hasPartialFailureError()) 
        {
            $this->log(sprintf("Partial failures occurred: '%s'",$response->getPartialFailureError()->getMessage()));

            $this->errors[] = $response->getPartialFailureError()->getMessage();
        } 
        else 
        {
            /** @var 
             * ConversionAdjustmentResult $uploadedConversionAdjustment 
             **/

            $uploadedConversionAdjustment = $response->getResults()[0];

            $this->log
            (
                sprintf
                (
                    "Uploaded conversion adjustment of %s for order ID %s with value of %s", $uploadedConversionAdjustment->getConversionAction(), $uploadedConversionAdjustment->getOrderId(), number_format($order->getGrandTotal(),2)
                )
            );
        }

        return $this->getErrors() ? false : true;
    }

    /**
     * Create enhaned conversion adjustment
     * 
     * @param int $conversionActionId
     * @param string $orderId
     * @param string|null $conversionDateTime
     * @param string|null $userAgent
     * 
     * @return [type]
     */
    public function enhance(int $orderId = 0, array $data = []) : bool
    {
        $this->errors = [];

        if (!$this->isConfigured())
        {
            $this->log(sprintf(__('Failed to upload conversion adjustment for order (ID:%s) due to incomplete Google Ads API configuration.', $orderId)));

            return true;
        }

        $order = null;

        try 
        {
             /**
             * Load order
             * 
             * @var \Magento\Sales\Model\Order
             */
            $order = $this->orderRepositoryInterface->get($orderId);
        }
        catch (Exception $e)
        {
            $this->log(sprintf(__('Order %s cannot be loaded. Failed to upload conversion adjustment'), $orderId));

            return true;
        }

        if (!$data)
        {
            /**
             * Initialize enhnacement data automatically
             */
            $data['email']              = $order->getCustomerEmail();
            $data['phone']              = $order->getShippingAddress()->getTelephone();
            $data['firstName']          = $order->getBillingAddress()->getFirstname();
            $data['lastName']           = $order->getBillingAddress()->getLastname();
            $data['countryCode']        = $order->getBillingAddress()->getCountryId();
            $data['postalCode']         = $order->getBillingAddress()->getPostcode();
            $data['conversionDateTime'] = date(static::DATE_FORMAT, strtotime($order->getCreatedAt()));
            $data['conversionActionId'] = $this->helper->getGoogleAdsApiConversionActionId();
        }
        
        /**
         * Set increement id
         */
        if (!isset($data['orderId']))
        {
            $data['orderId'] = $order->getIncrementId();
        }

        /**
         * Set conversion time
         */
        if (!isset($data['conversionDateTime']))
        {
            $data['conversionDateTime'] = date(static::DATE_FORMAT);
        }
  

        try 
        {
            $conversionActionId = $this->helper->getGoogleAdsApiConversionActionId();

            if (!$data['conversionActionId'])
            {
                $data['conversionActionId'] = $conversionActionId;
            }

            $enhancement =  new ConversionAdjustment(['adjustment_type' => ConversionAdjustmentType::ENHANCEMENT]);

            $userIdentifiers = [];

            $hashAlgorithm = "sha256";

            $userIdentifiers[] = new UserIdentifier
            (
                [
                    'hashed_email'              => $this->normalizeAndHashEmailAddress($hashAlgorithm,$data['email']),
                    'user_identifier_source'    => UserIdentifierSource::FIRST_PARTY
                ]
            );

            if (array_key_exists('phone', $data)) 
            {
                $userIdentifiers[] = new UserIdentifier(['hashed_phone_number' => $this->normalizeAndHash($hashAlgorithm,$data['phone'],true)]);
            }

            if (array_key_exists('firstName', $data)) 
            {
                $missingAddressKeys = [];

                foreach (['lastName', 'countryCode', 'postalCode'] as $addressKey) 
                {
                    if (!array_key_exists($addressKey, $data)) 
                    {
                        $missingAddressKeys[] = $addressKey;
                    }
                }
                if (!empty($missingAddressKeys)) 
                {
                    $this->log
                    (
                        sprintf("Skipping addition of mailing address information because the following required keys are missing: %s", json_encode($missingAddressKeys))
                    );
                } 
                else 
                {
                    $userIdentifiers[] = new UserIdentifier
                    (
                        [
                            'address_info' => new OfflineUserAddressInfo
                            (
                                [
                                    'hashed_first_name' => $this->normalizeAndHash($hashAlgorithm,$data['firstName'],false),
                                    'hashed_last_name'  => $this->normalizeAndHash($hashAlgorithm,$data['lastName'],false),
                                    'country_code'      => $data['countryCode'],
                                    'postal_code'       => $data['postalCode']
                                ]
                            )
                        ]
                    );
                }
            }

            $enhancement->setUserIdentifiers($userIdentifiers);

            $enhancement->setConversionAction
            (
                ResourceNames::forConversionAction($this->customer_id, $data['conversionActionId'])
            );

            if (!empty($data['orderId'])) 
            {
                $enhancement->setOrderId($data['orderId']);
            }

            if (!empty($data['conversionDateTime'])) 
            {
                $enhancement->setGclidDateTimePair(new GclidDateTimePair
                (
                    [
                        'conversion_date_time' => $data['conversionDateTime']
                    ])
                );
            }

            if (!empty($data['userAgent'])) 
            {
                $enhancement->setUserAgent($data['userAgent']);
            }

            $conversionAdjustmentUploadServiceClient = $this->getClient()->getConversionAdjustmentUploadServiceClient();

            $request = UploadConversionAdjustmentsRequest::build($this->customer_id, [$enhancement], true);


            $request->setPartialFailure(true);

            $response = $conversionAdjustmentUploadServiceClient->uploadConversionAdjustments($request);

            if ($response->hasPartialFailureError()) 
            {
                $this->log(sprintf("Partial failures occurred: '%s'",$response->getPartialFailureError()->getMessage()));

                $this->errors[] = $response->getPartialFailureError()->getMessage();
            } 
            else 
            {
                
                /** 
                 * @var ConversionAdjustmentResult $uploadedConversionAdjustment 
                 **/

                $uploadedConversionAdjustment = $response->getResults()[0];

                $this->log
                (
                    sprintf
                    (
                        "Uploaded conversion adjustment for order ID %s with value of %s", $uploadedConversionAdjustment->getOrderId(), number_format($order->getGrandTotal(),2)
                    )
                );
            }
        }
        catch (\Exception $e)
        {
            $this->errors[] = $this->getError($e);
            
            $this->log($e->getMessage());
        }

        return $this->getErrors() ? false : true;
    }

        /**
     * Get API state
     */
    public function getState() : array
    {
        $notes = [];
        $class = [];

        if (!$this->helper->getJsonKey())
        {
            $notes[] = __('Missing API JSON key.');
        }

        if (!$this->helper->getGoogleAdsApiDeveloperToken())
        {
            $notes[] = __('Missing developer token');
        }

        if (!$this->helper->getGoogleAdsApiCustomerId())
        {
            $notes[] = __('Missing Google Ads Customer Id');
        }

        if (!$this->helper->getGoogleAdsApiConversionActionId())
        {
            $notes[] = __('Missing Google Ads Conversion Action Id');
        }

        if (!$notes)
        {
            $notes[] = __('API is configured correctly.');

            $class[] = 'success';
        }
        else 
        {
            $class[] = 'failure';
        }

        return 
        [
            'notes' => $notes,
            'class' => $class
        ];
    }

    /**
     * Get conversion actions
     * 
     * @return array
     */
    public function getConversionActions() : array
    {
        if (!$this->isConfigured())
        {
            return [];
        }

        $actions = [];

        try 
        {
            $query = 'SELECT 
                        conversion_action.id, 
                        conversion_action.name 
                        
                    FROM conversion_action';

            $stream = $this->getClient()->getGoogleAdsServiceClient()->searchStream
            (
                SearchGoogleAdsStreamRequest::build($this->customer_id, $query)
            );

            foreach ($stream->iterateAllElements() as $row) 
            {
                $actions[] = $row;
            }
        }
        catch (\Exception $e)
        {   
            $this->errors[] = $this->getError($e);

            $this->log($e->getMessage());
        }

        return $actions;
    }

    /**
     * Create conversion action
     * 
     * @param string $action
     * 
     * @return [type]
     */
    public function createConversionAction(string $action = 'Purchase')
    {
        try 
        {
            $actions = [];

            foreach($this->getConversionActions() as $entity)
            {
                $actions[] = 
                [
                    'id' => $entity->getConversionAction()->getId(),
                    'name' => $entity->getConversionAction()->getName()
                ];
            }

            $conversionActionOperation = new ConversionActionOperation();

            $conversionActionOperation->setCreate(new ConversionAction
            (
                [
                    'name'                                  => $action,
                    'category'                              => ConversionActionCategory::PBDEFAULT,
                    'type'                                  => ConversionActionType::WEBPAGE,
                    'status'                                => ConversionActionStatus::ENABLED,
                    'view_through_lookback_window_days'     => 15,
                    'value_settings' => new ValueSettings
                    (
                        [
                            'default_value'             => 0,
                         'always_use_default_value'  => true
                        ]
                    )
                ]
            ));
            
            $conversionActionServiceClient = $this->getClient()->getConversionActionServiceClient();

            $response = $conversionActionServiceClient->mutateConversionActions
            (
                MutateConversionActionsRequest::build($this->customer_id, [$conversionActionOperation])
            );

            $actions = [];

            foreach ($response->getResults() as $addedConversionAction) 
            {
                $this->log(sprintf("New conversion action added with resource name: %s",$addedConversionAction->getResourceName()));

                $actions[$addedConversionAction->getConversionAction()->getId()] = $addedConversionAction->getConversionAction()->getName()();
            }

            return $addedConversionAction;
            
        }
        catch (\Exception $e)
        {
            $this->errors[] = $this->getError($e);

            $this->log($e->getMessage());
        }

        return null;

    }

    /**
     * Get setup information 
     * 
     * @return array
     */
    public function getSetup() : array
    {
        if (!$this->isConfigured())
        {
            return [];
        }

        $setup = [];

        try 
        {

            $query = 'SELECT
                            customer.conversion_tracking_setting.google_ads_conversion_customer,
                            customer.conversion_tracking_setting.conversion_tracking_status,
                            customer.conversion_tracking_setting.conversion_tracking_id,
                            customer.conversion_tracking_setting.cross_account_conversion_tracking_id
                        FROM customer';

            $stream = $this->getClient()->getGoogleAdsServiceClient()->searchStream
            (
                SearchGoogleAdsStreamRequest::build($this->customer_id, $query)
            );

            foreach ($stream->iterateAllElements() as $row) 
            {
                $setup[] = $row;
            }
        }
        catch (Exception $e)
        {
            $this->errors[] = $this->getError($e);

            $this->log($e->getMessage());
        }

        return $setup;
    }

    public function getHealth()
    { 
        $health = [];

        try 
        {

            $query = 'SELECT
                        offline_conversion_upload_conversion_action_summary.conversion_action_name,
                        offline_conversion_upload_conversion_action_summary.alerts,
                        offline_conversion_upload_conversion_action_summary.client,
                        offline_conversion_upload_conversion_action_summary.daily_summaries,
                        offline_conversion_upload_conversion_action_summary.job_summaries,
                        offline_conversion_upload_conversion_action_summary.last_upload_date_time,
                        offline_conversion_upload_conversion_action_summary.pending_event_count,
                        offline_conversion_upload_conversion_action_summary.status,
                        offline_conversion_upload_conversion_action_summary.successful_event_count,
                        offline_conversion_upload_conversion_action_summary.total_event_count
                        FROM offline_conversion_upload_conversion_action_summary
                WHERE offline_conversion_upload_conversion_action_summary.conversion_action_id = ' . $this->helper->getGoogleAdsApiConversionActionId();

            $stream = $this->getClient()->getGoogleAdsServiceClient()->searchStream
            (
                SearchGoogleAdsStreamRequest::build($this->customer_id, $query)
            );

            foreach ($stream->iterateAllElements() as $row) 
            {
                $health[] = $this->interpret($row);
            }
            
        }
        catch (Exception $e)
        {
            $this->errors[] = $this->getError($e);

            $this->log($e->getMessage());
        }

        return $health;
        
    }

    /**
     * Interpret row
     * 
     * @param \Google\Ads\GoogleAds\V20\Services\GoogleAdsRow $row
     * 
     * @return [type]
     */
    private function interpret(\Google\Ads\GoogleAds\V20\Services\GoogleAdsRow $row)
    {
        return json_decode($row->getOfflineConversionUploadConversionActionSummary()->serializeToJsonString(), true);
    }

    /**
     * Check if eligible for adjustment
     * 
     * @param int $orderId
     * 
     * @return bool
     */
    public function isEligible(int $orderId = 0) : bool
    {
        try 
        {
            $order = $this->orderRepositoryInterface->get($orderId);

            $time = time() - strtotime($order->getCreatedAt());

            if ($time < (24 * 60))
            {
                return true;
            }
        }
        catch (\Exception $e){}

        return false;
    }

    /**
     * Create offline data job client
     * 
     * @param OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient
     * @param string|null $offlineUserDataJobType
     * 
     * @return string
     */
    private function createOfflineUserDataJob(OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient, ?string $offlineUserDataJobType): string 
    {
        $storeSalesMetadata = new StoreSalesMetadata(
        [
            'loyalty_fraction'              => 0.7, 
            'transaction_upload_fraction'   => 1.0,
        ]);

        $offlineUserDataJob = new OfflineUserDataJob(
        [
            'type'                 => OfflineUserDataJobType::value($offlineUserDataJobType),
            'store_sales_metadata' => $storeSalesMetadata
        ]);


        /** @var 
         * CreateOfflineUserDataJobResponse $createOfflineUserDataJobResponse 
         **/
        $createOfflineUserDataJobResponse = $offlineUserDataJobServiceClient->createOfflineUserDataJob
        (
            CreateOfflineUserDataJobRequest::build($this->customer_id, $offlineUserDataJob)
        );

        $offlineUserDataJobResourceName = $createOfflineUserDataJobResponse->getResourceName();

        $this->log(sprintf("Created an offline user data job with resource name: '%s'", $offlineUserDataJobResourceName));

        return $offlineUserDataJobResourceName;

    }

    private function addTransactionsToOfflineUserDataJob( OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient, \Magento\Sales\Model\Order $order, string $offlineUserDataJobResourceName, ?int $adPersonalizationConsent, ?int $adUserDataConsent,  array $items = [] ) 
    {
        $userDataJobOperations = $this->buildOfflineUserDataJobOperations
        (
            $order,
            $this->helper->getGoogleAdsApiConversionActionId(),
            $adPersonalizationConsent,
            $adUserDataConsent,
            $items
        );

        $request = AddOfflineUserDataJobOperationsRequest::build($offlineUserDataJobResourceName,$userDataJobOperations);

        $request->setEnablePartialFailure(true)->setEnableWarnings(true);

        $response = $offlineUserDataJobServiceClient->addOfflineUserDataJobOperations($request);

        if ($response->hasPartialFailureError()) 
        {
            $this->log(sprintf("Encountered %d partial failure errors while adding %d operations to the offline user data job: '%s'. Only the successfully added operations will be executed when the job runs.", count($response->getPartialFailureError()->getDetails()), count($userDataJobOperations), $response->getPartialFailureError()->getMessage()));
        } 
        else 
        {
            $this->log(sprintf("Successfully added %d operations to the offline user data job", count($userDataJobOperations)));
        }

        if ($response->hasWarning()) 
        {
            $warningFailure = GoogleAdsFailures::fromAnys($response->getWarning()->getDetails());

            $this->log(sprintf("Encountered %d warning(s)", count($warningFailure->getErrors())));
        }
    }

    /**
     * Build offline job 
     * 
     * @param \Magento\Sales\Model\Order|null $order
     * @param mixed $conversionActionId
     * @param int|null $adPersonalizationConsent
     * @param int|null $adUserDataConsent
     * @param int|null $merchantCenterAccountId
     * @param array $items
     * 
     * @return array
     */
    private function buildOfflineUserDataJobOperations(?\Magento\Sales\Model\Order $order, $conversionActionId,?int $adPersonalizationConsent,?int $adUserDataConsent, array $items = []): array 
    {

        $date = function()
        {
            return date('y-m-d h:i:s');
        };

        $userDataWithEmailAddress = new UserData(
        [
            'user_identifiers' => 
            [
                new UserIdentifier(
                [
                    'hashed_email' => $this->normalizeAndHash($order->getBillingAddress()->getEmail())
                ]),
                new UserIdentifier(
                [
                    'address_info' => new OfflineUserAddressInfo
                    (
                            [
                                
                            ]
                    )
                ])
            ],
            'transaction_attribute' => new TransactionAttribute
            (
                [
                'conversion_action'         => ResourceNames::forConversionAction($this->customer_id, $conversionActionId),
                'currency_code'             => $order->getOrderCurrencyCode(),
                'transaction_amount_micros' => Helper::baseToMicro($order->getGrandTotal()),
                'transaction_date_time'     => $date()
            ])
        ]);

        if (!empty($adPersonalizationConsent) || !empty($adUserDataConsent)) 
        {
            $consent = new Consent();

            if (!empty($adPersonalizationConsent)) 
            {
                $consent->setAdPersonalization($adPersonalizationConsent);
            
                if (!empty($adUserDataConsent)) 
                {
                    $consent->setAdUserData($adUserDataConsent);
                }

                $userDataWithEmailAddress->setConsent($consent);
            }
        }

        // Creates the second transaction for upload based on a physical address.
        $userDataWithPhysicalAddress = new UserData(
        [
            'user_identifiers' => 
            [
                new UserIdentifier(
                [
                    'address_info' => new OfflineUserAddressInfo
                    (
                        [
                            'hashed_first_name'     => $this->normalizeAndHash($order->getBillingAddress()->getFirstname()),
                            'hashed_last_name'      => $this->normalizeAndHash($order->getBillingAddress()->getLastname()),
                            'country_code'          => $order->getBillingAddress()->getCountryId(),
                            'postal_code'           => $order->getBillingAddress()->getTelephone()
                        ]
                    )
                ])
            ],
            'transaction_attribute' => new TransactionAttribute(
            [
                'conversion_action'         => ResourceNames::forConversionAction($this->customer_id, $conversionActionId),
                'currency_code'             => $order->getOrderCurrencyCode(),
                'transaction_amount_micros' => Helper::baseToMicro($order->getGrandTotal()),
                'transaction_date_time'     => $date()
            ])
        ]);

        // Optional: If uploading data with item attributes, also assign these values
        // in the transaction attribute.
        if (!empty($itemId)) 
        {
            $userDataWithPhysicalAddress->getTransactionAttribute()->setItemAttribute
            (
                new ItemAttribute(
                [
                    'item_id'       => $itemId,
                    'merchant_id'   => $merchantCenterAccountId,
                    'country_code'  => $order->getBillingAddress()->getCountryId(),
                    'language_code' => $languageCode,
                    'quantity'      => 1
                ])
            );
        }

        // Creates the operations to add the two transactions.
        $operations = [];

        foreach ([$userDataWithEmailAddress, $userDataWithPhysicalAddress] as $userData) 
        {
            $operations[] = new OfflineUserDataJobOperation(['create' => $userData]);
        }

        return $operations;
    }

    public function getAdjustmentData(int $orderId = 0)
    {
        $data = [];

        try 
        {
             /**
             * Load order
             * 
             * @var \Magento\Sales\Model\Order
             */
            $order = $this->orderRepositoryInterface->get($orderId);

            $data['order_id'] = $order->getIncrementId();
            $data['value'] = $order->getGrandTotal();

        }
        catch (\Exception $e){}

        return $data;
    }

    /**
     * Get conversion fields
     * 
     * @param int $orderId
     * 
     * @return [type]
     */
    public function getFields(int $orderId = 0)
    {
        $fields = [];

        try 
        {
             /**
             * Load order
             * 
             * @var \Magento\Sales\Model\Order
             */
            $order = $this->orderRepositoryInterface->get($orderId);

            $fields = 
            [
                'conversionActionId' => 
                [
                    'type'      => 'text',
                    'label'     => 'Conversion Action Id',
                    'value'     => $this->helper->getGoogleAdsApiConversionActionId(),
                    'note'      => __('Action ID can be configured in configuration screen'),
                    'readonly'  => true
                ],
                'conversionValue' => 
                [
                    'type'  => 'text',
                    'label' => 'Conversion Value',
                    'value' => number_format($order->getGrandTotal(),2),
                ],
                'email' => 
                [
                    'type'  => 'text',
                    'label' => 'Email',
                    'value' => $order->getBillingAddress()->getEmail(),
                ],
                'firstName' => 
                [
                    'type'  => 'text',
                    'label' => 'Firstname',
                    'value' => $order->getBillingAddress()->getFirstname(),
                ],
                'lastName' => 
                [
                    'type'  => 'text',
                    'label' => 'Lastname',
                    'value' => $order->getBillingAddress()->getLastname(),
                ],
                'countryCode' => 
                [
                    'type'  => 'text',
                    'label' => 'Country ID',
                    'value' => $order->getBillingAddress()->getCountryId(),
                ],
                'postalCode' => 
                [
                    'type'  => 'text',
                    'label' => 'Postcode',
                    'value' => $order->getBillingAddress()->getPostcode(),
                ],
                'phone' => 
                [
                    'type'  => 'text',
                    'label' => 'Phone',
                    'value' => $order->getBillingAddress()->getTelephone(),
                ],
                'currencyCode' => 
                [
                    'type'  => 'text',
                    'label' => 'Currency',
                    'value' => $order->getOrderCurrencyCode()
                ]
            ];
        }
        catch (Exception $e)
        {
            $this->log(sprintf(__('Order %s cannot be loaded. Failed to upload conversion adjustment'), $orderId));

            return true;
        }

        return 
        [
            'order_id'  => $order->getIncrementId(),
            'fields'    => $fields
        ];
    }

    
    /**
     * Normalize and hash email address
     * 
     * @param string $hashAlgorithm
     * @param string $emailAddress
     * 
     * @return string
     */
    private function normalizeAndHashEmailAddress( string $hashAlgorithm, string $emailAddress) : string 
    {
        $normalizedEmail = strtolower($emailAddress);

        $emailParts = explode("@", $normalizedEmail);

        if (count($emailParts) > 1 && preg_match('/^(gmail|googlemail)\.com\s*/', $emailParts[1])) 
        {
            $emailParts[0] = str_replace(".", "", $emailParts[0]);

            $normalizedEmail = sprintf('%s@%s', $emailParts[0], $emailParts[1]);
        }
        return $this->normalizeAndHash($hashAlgorithm, $normalizedEmail, true);
    }
    
    /**
     * Normalize and hash
     * 
     * @param string $hashAlgorithm
     * @param string $value
     * @param bool $trimIntermediateSpaces
     * 
     * @return string
     */
    private function normalizeAndHash( string $hashAlgorithm,string $value,bool $trimIntermediateSpaces): string 
    {
        $normalized = strtolower($value);

        if ($trimIntermediateSpaces === true) 
        {
            // Removes leading, trailing, and intermediate spaces.
            $normalized = str_replace(' ', '', $normalized);
        } 
        else 
        {
            $normalized = trim($normalized);
        }

        return hash($hashAlgorithm, strtolower(trim($normalized)));
    }

    /**
     * Format customer id
     * 
     * @param string $customer_id
     * 
     * @return string
     */
    private function formatCustomerId(string $customer_id = '') : string
    {
        return join('', explode('-', $customer_id));
    }



    /**
     * Log message 
     * 
     * @param string $message
     * 
     * @return [type]
     */
    private function log(string $message = '')
    {
        try 
        {
            $log = $this->logFactory->create();

            $log->setLog($message);

            $this->logRepository->save($log);
        }
        catch (\Exception $e){}

        return $this;
    }
    
    /**
     * Check if API is configured 
     * 
     * @return bool
     */
    public function isConfigured() : bool
    {
        if (!$this->helper->getJsonKey())
        {
            return false;
        }

        if (!$this->helper->getGoogleAdsApiDeveloperToken())
        {
            return false;
        }

        if (!$this->helper->getGoogleAdsApiCustomerId())
        {
            return false;
        }

        return true;
    }

    /**
     * Get error as text
     */
    private function getError(Exception $e) : string
    {
        $error = [];

        switch(true)
        {
            case $e instanceof ApiException:

                $data = json_decode($e->getMessage());

                if ($data)
                {
                    $error[] = $data->status;
                    $error[] = $data->message;

                    foreach($data->details as $detail)
                    {
                        foreach($detail->errors as $entity)
                        {
                            if (property_exists($entity->errorCode,'authorizationError'))
                            {
                                $error[] = $entity->errorCode->authorizationError;
                            }

                            if (property_exists($entity->errorCode,'requestError'))
                            {
                                $error[] = $entity->errorCode->requestError;
                            }    
                            
                            $error[] = $entity->message;
                        }
                    }
                    
                }

                break;
            case $e instanceof RequestException:

                $error[] = $e->getMessage();

                break;
        }

        return join(chr(32), $error);
    }

    
    /**
     * Get errors 
     * 
     * @return array
     */
    public function getErrors() : array
    {
        return $this->errors;
    }

    /**
     * Check if API has errors 
     * 
     * @return bool
     */
    public function hasErrors() : bool
    {
        return count($this->errors);
    }

    /**
     * Get client 
     * 
     * @return GoogleAdsClient
     */
    private function getClient() : GoogleAdsClient
    {
        if (!$this->client)
        {
            $this->client = (new GoogleAdsClientBuilder())->withDeveloperToken($this->helper->getGoogleAdsApiDeveloperToken())->withOAuth2Credential((new OAuth2TokenBuilder())->withJsonKeyFilePath($this->helper->getJsonKey())->withScopes(['https://www.googleapis.com/auth/adwords'])->build())->build();
        }

        return $this->client;
    }
}