<?php

namespace App\Services\Navision;

use App\Jobs\Shopify\SyncCompany;
use App\Jobs\Shopify\SyncCustomer;
use App\Models\Address;
use App\Models\Company;
use App\Models\Customer;
use App\Models\SyncControl;
use App\Support\HubSpotClient;
use App\Support\NavClient;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Navision API service class for clients (contacts & companies)
 */
class ContactsService {

    /**
     * Fields used to create the signature that will detect meaningful changes to company (to avoid unecessary update calls to Shopify)
     */
    const COMPANY_SINGATURE_FIELDS = [
        'Name', 'Name_2', 'Address', 'Address_2', 'Post_Code', 'City', 'County', 'Country_Region_Code', 'Phone_No', 'Customer_Disc_Group', 'Tax_Liable', 'VAT_Registration_No'
    ];
    /**
     * Fields used to create the signature that will detect meaningful changes to contact (to avoid unecessary update calls to Shopify)
     */
    const CONTACT_SIGNATURE_FIELDS = [
        'First_Name', 'Surname', 'E_Mail', 'Country_Region_Code', 'Phone_No', 'Customer_Number', 'Market', 'Webshop_Access'
    ];
    const ADDRESS_MANDATORY_FIELDS = ['Name', 'Address', 'City', 'Country_Region_Code', 'Post_Code'];
    const AMERICAN_PHONE_CODE_COUNTRIES = [
        'CA', 'US', 'PR', 'AS', 'AG', 'AI', 'BS', 'BB', 'BM', 'VG', 'KY', 'DM', 'DO', 'GD', 'GU',
        'JM', 'MS', 'MP', 'PR', 'KN', 'LC', 'VC', 'SX', 'TT', 'TC', 'VI'
    ];

    protected $client;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->client = new NavClient();
    }

    /**
     * Counts NAV companies
     *
     * @return int
     */
    public function countCompanies()
    {
        $bookmark = null;
        $count = 0;
        do {
            $companiesData = $this->client->getCompanies([
                'Customer' => 'true'
            ], 200, $bookmark);
            $pageCount = sizeof($companiesData);
            $count += $pageCount;
            $bookmark = $companiesData[$pageCount - 1]->Key;
        } while (sizeof($companiesData) == 200);
        return $count;
    }

    /**
     * Counts Nav Contacts
     *
     * @return void
     */
    public function countContacts()
    {
        $bookmark = null;
        $count = 0;
        do {
            $companiesData = $this->client->getContacts([
                'Webshop_Access' => 'true'
            ], 200, $bookmark);
            $pageCount = sizeof($companiesData);
            $count += $pageCount;
            $bookmark = $companiesData[$pageCount - 1]->Key;
        } while (sizeof($companiesData) == 200);
        return $count;
    }

    /**
     * Gets modified companies data from Navision and sync with local data
     * Dispatches shopify update job for each modified company if not in force mode
     *
     * @param boolean $forceUpdate - Ignores last modified and signature hash
     * @return void
     */
    public function getAndSyncCompanies($forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 100;
        $count = 0;
        $syncTime = Carbon::now();
        $totalSuccess = 0;
        $filter = [];

        $syncControl = SyncControl::where('type', SyncControl::TYPE_COMPANIES)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = SyncControl::TYPE_COMPANIES;
        }

        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }

        do {
            $companiesData = $this->client->getCompanies($filter, $perCall, $bookmark);
            $pageCount = sizeof($companiesData);

            if ($pageCount == 0) {
                break;
            }

            $count += $pageCount;
            $bookmark = $companiesData[$pageCount - 1]->Key;

            foreach ($companiesData as $companyData) {
                // Only updates existing companies
                if (!Company::where('nav_no', $companyData->No)->exists()) {
                    continue;
                }

                $company = $this->syncCompany($companyData, $forceUpdate);
                if ($company) {
                    $totalSuccess++;

                    if ($company->to_sync && !$forceUpdate) {
                        SyncCompany::dispatch($company->id);
                    }
                }
            }
        } while (sizeof($companiesData) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();

        Log::info('Synced ' . $totalSuccess . ' companies out of ' . $count . ' from Navision.');
    }

    /**
     * Gets the company data specified by $companyNo (NAV id) and syncs with local data
     * Force mode ignores signature check
     * Dispaches sync Job to shopify if not in force mode
     *
     * @param string $companyNo
     * @param boolean $forceUpdate
     * @return Company
     */
    public function getAndSyncCompany(string $companyNo, bool $forceUpdate = false)
    {
        $companyData = $this->client->getCompany($companyNo);
        $company = $this->syncCompany($companyData, $forceUpdate);

        if ($company && $company->to_sync && !$forceUpdate) {
            SyncCompany::dispatch($company->id);
        }

        return $company;
    }

    /**
     * Gets the contact data specified by $contactNo (NAV id) and syncs with local data
     * Force mode ignores signature check
     * Dispaches sync Job to shopify if not in force mode
     *
     * @param string $contactNo
     * @param boolean $forceUpdate
     * @return Customer | false
     */
    public function getAndSyncContact(string $contactNo, bool $forceUpdate = false)
    {
        $contactData = $this->client->getContact($contactNo);
        $customer = $this->syncContact($contactData, $forceUpdate);

        if ($customer && $customer->to_sync && !$forceUpdate) {
            SyncCustomer::dispatch($customer->id);
        }

        return $customer;
    }

    /**
     * Syncs company data from NAV with local data
     * Force update ignores hash signature check
     *
     * @param object $companyData
     * @param boolean $forceUpdate
     * @return Company | false
     */
    public function syncCompany(object $companyData, bool $forceUpdate = false)
    {
        if (empty($companyData->Name)) {
            Log::warning('Unable to sync company: no name field - ' . $companyData->No);
            return false;
        }

        DB::beginTransaction();
        try {
            $company = Company::where('nav_no', $companyData->No)->first();
            if (!$company) {
                $company = new Company();
                $company->nav_no = $companyData->No;
            }

            if ($forceUpdate) {
                $company->sync_hash = '';
            }

            if (!$this->_checkCompanyUpdate($company, $companyData)) {
                DB::commit();
                Log::info('Company skipped - same hash: ' . $companyData->No . ' - Internal ID: ' . $company->id);
                return $company;
            }

            $billingAddress = $this->_syncAddress($companyData, $company->billingAddress);
            $company->billing_address_id = $billingAddress ? $billingAddress->id : null;

            $company->name = $companyData->Name;
            if (!empty($companyData->Name_2) && $companyData->Name_2 != $companyData->Name) {
                $company->name .= ' | ' . $companyData->Name_2;
            }
            $company->discount_group = !empty($companyData->Customer_Disc_Group) ? $companyData->Customer_Disc_Group : 'COM';
            $company->country_code = !empty($companyData->Country_Region_Code) ? $companyData->Country_Region_Code : '';
            $company->state_code = !empty($companyData->County) ? $companyData->County : '';
            $company->is_tax_exempt = !empty($companyData->Tax_Liable) && $companyData->Tax_Liable ? false : true;
            $company->tax_number = !empty($companyData->VAT_Registration_No) ? $companyData->VAT_Registration_No : null;
            $company->save();

            DB::commit();
            Log::info('Company Updated/Created: ' . $companyData->No . ' - Internal ID: ' . $company->id);
            return $company;
        } catch (\Throwable $e) {
            Log::error('Error Creating/Updating Company: ' . $companyData->No);
            Log::error($e);
            DB::rollBack();
            return false;
        }
    }

    /**
     * Syncs contact data from NAV with local data
     * Force update ignores hash signature check
     *
     * @param object $contactData
     * @param boolean $forceUpdate
     * @return Customer | false
     */
    public function syncContact(object $contactData, bool $forceUpdate = false)
    {
        if (empty($contactData->Customer_Number)) {
            Log::warning('Unable to sync contact: no Customer_Number provided - ' . $contactData->No);
            return false;
        }
        DB::beginTransaction();
        try {
            $company = Company::where('nav_no', $contactData->Customer_Number)->first();
            if (!$company) {
                // Imports company first
                $company = $this->getAndSyncCompany($contactData->Customer_Number, $forceUpdate);

                if (!$company) {
                    Log::warning('Unable to sync contact: Company not found - ' . $contactData->No . ' / ' . $contactData->Customer_Number);
                    DB::rollback();
                    return false;
                }
            }

            $customer = Customer::where('nav_no', $contactData->No)->first();

            // Checks if email already taken by other contact
            if (!empty($contactData->E_Mail) && (!$customer || $customer->email != $contactData->E_Mail)) {
                $emailCustomer = Customer::where('email', $contactData->E_Mail)
                    ->where('nav_no', '<>', $contactData->No)
                    ->first();

                if ($emailCustomer) {
                    Log::warning('Unable to sync contact: Email already taken - ' . $contactData->No . ' / ' . $contactData->E_Mail);
                    DB::rollback();
                    return false;
                }
            }

            if (!$customer) {
                $customer = new Customer();
                $customer->nav_no = $contactData->No;
            }

            if ($forceUpdate) {
                $customer->sync_hash = '';
            }

            if (!$this->_checkContactUpdate($customer, $contactData)) {
                DB::commit();
                Log::info('Contact skipped - same hash: ' . $contactData->No . ' - Internal ID: ' . $customer->id);
                return $customer;
            }

            $customer->first_name = !empty($contactData->First_Name) ? $contactData->First_Name : '';
            $customer->last_name = !empty($contactData->Surname) ? $contactData->Surname : '';
            $customer->email = $contactData->E_Mail;
            $countryCode = !empty($contactData->Country_Region_Code) ? $contactData->Country_Region_Code : '';
            $customer->phone = !empty($contactData->Phone_No) ? $this->_formatPhoneNumber($contactData->Phone_No, $countryCode) : null;
            $customer->company_id = $company->id;
            $customer->market = !empty($contactData->Market) ? $contactData->Market : null;
            $customer->save();

            /*
            if (!$company->main_customer_id) {
                $company->main_customer_id = $customer->id;
                $company->to_sync = true;
                $company->save();
            }
            */
            DB::commit();
            Log::info('Customer Updated/Created: ' . $contactData->No . ' - Internal ID: ' . $customer->id);
            return $customer;
        } catch (\Throwable $e) {
            Log::error('Error Creating/Updating Customer: ' . $contactData->No);
            Log::error($e);
            DB::rollBack();
            return false;
        }
    }

    /**
     * Gets modified contacts data from Navision and syncs with local data
     * Dispatches shopify update job for each modified contact if not in force mode
     *
     * @param boolean $forceUpdate - Ignores last modified and signature hash
     * @return void
     */
    public function getAndSyncContacts($forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 100;
        $count = 0;
        $syncTime = Carbon::now();
        $totalSuccess = 0;

        $filter = [
            'Type' => 'Person',
            'Webshop_Access' => 'true',
        ];

        $syncControl = SyncControl::where('type', SyncControl::TYPE_CONTACTS)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = SyncControl::TYPE_CONTACTS;
        }

        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }

        do {
            $contactsData = $this->client->getContacts($filter, $perCall, $bookmark);
            $pageCount = sizeof($contactsData);
            Log::info('Fetched ' . $pageCount . ' contacts from Navision.');
            if ($pageCount == 0) {
                break;
            }

            $count += $pageCount;
            $bookmark = $contactsData[$pageCount - 1]->Key;

            foreach ($contactsData as $contactData) {
                $customer = $this->syncContact($contactData, $forceUpdate);
                if ($customer) {
                    $totalSuccess++;

                    if ($customer->to_sync && !$forceUpdate) {
                        SyncCustomer::dispatch($customer->id);
                    }
                }
            }
        } while (sizeof($contactsData) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();

        Log::info('Synced ' . $totalSuccess . ' contacts out of ' . $count . ' from Navision.');
    }

    /**
     * Forces a NAV customer sync overriding NAV webshop access value
     *
     * @param integer $hubspotContactId
     * @param string $webshopAccessValue
     * @return void
     */
    public function updateCustomerFromHubspot(int $hubspotContactId)
    {
        $hubspot = new HubSpotClient();
        $hubspotContact = $hubspot->getContactById($hubspotContactId);

        if (!$hubspotContact) {
            return;
        }

        if (empty($hubspotContact['navNo'])) {
            Log::warning('HubSpot Customer Update: Unable to sync contact - Hubspot data without Customer Number for contact id: ' . $hubspotContactId);
        }

        $customer = Customer::where('nav_no', $hubspotContact['navNo'])->first();

        if ($customer) {
            Log::info('HubSpot Customer Update: No need to update contact since a contact already exists: ' . $hubspotContact['navNo']);
            return;
        }

        $this->getAndSyncContact($hubspotContact['navNo']);
    }

    /**
     * Syncs a given address data with local Address
     *
     * @param object $sourceData
     * @param Address|null $address - an optional existing address (update)
     * @return Address
     */
    protected function _syncAddress(object $sourceData, Address $address = null)
    {
        if (!$address) {
            $address = new Address();
        }
        foreach (self::ADDRESS_MANDATORY_FIELDS as $field) {
            if (empty($sourceData->$field)) {
                Log::warning('Unable to sync address. Missing field: ' . $field);
                return false;
            }
        }
        $address->name = $sourceData->Name;
        $address->address1 = $sourceData->Address;
        $address->address2 = !empty($sourceData->Address_2) ? $sourceData->Address_2 : null;
        $address->city = $sourceData->City;
        $address->country_code = $sourceData->Country_Region_Code;
        $address->zip = $sourceData->Post_Code;
        $address->state_code = !empty($sourceData->County) ? $sourceData->County : null;
        if (!empty($sourceData->Phone_No)) {
            $address->phone = $this->_formatPhoneNumber($sourceData->Phone_No, $sourceData->Country_Region_Code);
        }
        $address->save();

        return $address;
    }

    /**
     * Formats phone numbers to minimize Shopify validation errors
     *
     * @param string $phoneNumber
     * @param string $countryCode
     * @return string formatted phone number
     */
    protected function _formatPhoneNumber(string $phoneNumber, string $countryCode)
    {
        $onlyNumbers = preg_replace('/[^0-9]/', '', $phoneNumber);

        // Checks if number has country code already
        if ($phoneNumber[0] == '+') {
            return "+$onlyNumbers";
        }

        // Checks if country is part of the north american country code (+1)
        if ($countryCode && in_array($countryCode, self::AMERICAN_PHONE_CODE_COUNTRIES)) {
            if (strlen($onlyNumbers) == 11) {
                return "+$onlyNumbers";
            }
            if (strlen($onlyNumbers) == 10) {
                return  "+1$onlyNumbers";
            }
        }
        return '';
    }

    /**
     * Checks and generates a signature to control company data updates
     *
     * @param Company $company
     * @param object $navCompany
     * @return boolean - returns true if company needs updating
     */
    protected function _checkCompanyUpdate(Company $company, object $navCompany)
    {
        $signature = [];

        foreach (self::COMPANY_SINGATURE_FIELDS as $field) {
            $signature[] = isset($navCompany->$field) ? $navCompany->$field : '';
        }

        return $this->_objectHashCheck($company, $signature);
    }

    /**
     * Checks and generates a signature to control contact data updates
     *
     * @param Customer $customer
     * @param object $navCustomer
     * @return boolean - returns true if company needs updating
     */
    protected function _checkContactUpdate(Customer $customer, object $navCustomer)
    {
        $signature = [];

        foreach (self::CONTACT_SIGNATURE_FIELDS as $field) {
            $signature[] = isset($navCustomer->$field) ? $navCustomer->$field : '';
        }

        return $this->_objectHashCheck($customer, $signature);
    }

    /**
     * Checks object signature against current model and marks it to be synced if needed
     *
     * @param Model $object
     * @param array $signature
     * @return bool - true if object should be synced
     */
    protected function _objectHashCheck(Model $object, array $signature)
    {
        $currentHash = md5(serialize($signature));
        if ($currentHash === $object->sync_hash) {
            return false;
        }

        $object->sync_hash = $currentHash;
        $object->last_sync_at = Carbon::now();
        $object->to_sync = true;
        return true;
    }
}
