<?php

namespace classes;

use objects\logs_o;
use objects\users_o;

class customer_mass_import_service
{
    /**
     * Import or create a company customer from one normalized spreadsheet row.
     *
     * @throws \RuntimeException
     */
    public function import(array $payload): array
    {
        $normalized = $this->normalizePayload($payload);
        $this->assertValidNormalizedPayload($normalized);

        $customerNumber = (int)$normalized['customer_number'];
        $cvr = (string)$normalized['cvr'];
        $warnings = [];

        $economicCustomers = $this->searchEconomicCustomersByCvr($cvr);
        $localUserExistsBefore = $this->localCustomerNumberExists($customerNumber);
        $localUser = $localUserExistsBefore ? $this->loadLocalCustomerByNumber($customerNumber) : null;
        $matchingEconomicCustomer = $this->findEconomicCustomerByNumber($economicCustomers, $customerNumber);

        if ($matchingEconomicCustomer !== null) {
            $customer = $this->resolveLocalCustomer($customerNumber, $localUserExistsBefore, $localUser);
            $this->syncLocalCustomer($customer, $normalized, $warnings);

            [$action, $message] = $this->resolveExistingCustomerOutcome($localUserExistsBefore, $this->hasLocalAccount($customer));

            return $this->buildSuccessResult(
                $normalized,
                $customer,
                $action,
                $message,
                $localUserExistsBefore,
                true,
                false,
                $warnings
            );
        }

        if (count($economicCustomers) > 0) {
            $existingEconomicCustomerNumber = $this->extractEconomicCustomerNumber($economicCustomers[0]);
            $this->logIssue('CUSTOMER_MASS_IMPORT_CONFLICT', [
                'phase' => 'search',
                'cvr' => $cvr,
                'requestedCustomerNumber' => $customerNumber,
                'existingCustomerNumber' => $existingEconomicCustomerNumber,
            ]);

            throw new \RuntimeException(
                'CVR already registered under customer number '
                . $existingEconomicCustomerNumber
                . '. The submitted phone number must match the customer id. Manual cleanup or reassignment is required before retrying.',
                409
            );
        }

        $normalized['name'] = $this->resolveCreateName($normalized);
        $normalized['email'] = $this->resolveCreateEmail($normalized, $warnings);

        $createResponse = $this->createEconomicCustomer($normalized);
        $createdCustomerNumber = $this->extractEconomicCustomerNumber($createResponse);

        if ($createdCustomerNumber !== $customerNumber) {
            $this->logIssue('CUSTOMER_MASS_IMPORT_CONFLICT', [
                'phase' => 'create',
                'cvr' => $cvr,
                'requestedCustomerNumber' => $customerNumber,
                'createdCustomerNumber' => $createdCustomerNumber,
                'response' => $createResponse,
            ]);

            throw new \RuntimeException(
                'E-conomic created the customer under customer number '
                . $createdCustomerNumber
                . ' instead of the submitted phone number '
                . $customerNumber
                . '. Manual cleanup or reassignment is required before retrying.',
                409
            );
        }

        $customer = $this->resolveLocalCustomer($customerNumber, $localUserExistsBefore, $localUser);
        $this->syncLocalCustomer($customer, $normalized, $warnings);

        [$action, $message] = $this->resolveCreatedCustomerOutcome($localUserExistsBefore, $this->hasLocalAccount($customer));

        return $this->buildSuccessResult(
            $normalized,
            $customer,
            $action,
            $message,
            $localUserExistsBefore,
            false,
            true,
            $warnings
        );
    }

    protected function normalizePayload(array $payload): array
    {
        return [
            'customer_number' => $this->normalizePositiveInt($payload['customer_number'] ?? $payload['phone'] ?? null),
            'phone' => $this->normalizePositiveInt($payload['phone'] ?? $payload['customer_number'] ?? null),
            'cvr' => $this->normalizeDigitString($payload['cvr'] ?? null),
            'name' => $this->normalizeText($payload['name'] ?? $payload['company_name'] ?? null),
            'email' => $this->normalizeEmail($payload['email'] ?? null),
            'ean' => $this->normalizeDigitString($payload['ean'] ?? null),
        ];
    }

    protected function assertValidNormalizedPayload(array $normalized): void
    {
        $customerNumber = $normalized['customer_number'];
        $cvr = $normalized['cvr'];

        if ($customerNumber === null) {
            throw new \RuntimeException('Phone number is required.', 400);
        }

        $customerNumberLength = strlen((string)$customerNumber);
        if ($customerNumberLength < 8 || $customerNumberLength > 10) {
            throw new \RuntimeException('Phone number must be between 8 and 10 digits.', 400);
        }

        if ($cvr === null) {
            throw new \RuntimeException('CVR is required.', 400);
        }

        $cvrLength = strlen($cvr);
        if ($cvrLength < 8 || $cvrLength > 20) {
            throw new \RuntimeException('CVR must be between 8 and 20 digits.', 400);
        }

        if ($normalized['ean'] !== null && strlen((string)$normalized['ean']) > 13) {
            throw new \RuntimeException('EAN must be at most 13 digits.', 400);
        }
    }

    protected function normalizePositiveInt(mixed $value): ?int
    {
        $digits = $this->normalizeDigitString($value);
        if ($digits === null) {
            return null;
        }

        $normalized = (int)$digits;
        return $normalized > 0 ? $normalized : null;
    }

    protected function normalizeDigitString(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        $digits = preg_replace('/\D+/', '', (string)$value);
        if (!is_string($digits)) {
            return null;
        }

        $digits = trim($digits);
        return $digits !== '' ? $digits : null;
    }

    protected function normalizeText(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        $normalized = trim((string)$value);
        return $normalized !== '' ? $normalized : null;
    }

    protected function normalizeEmail(mixed $value): ?string
    {
        $email = $this->normalizeText($value);
        if ($email === null) {
            return null;
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \RuntimeException('Invalid email address.', 400);
        }

        return $email;
    }

    protected function resolveCreateName(array $normalized): string
    {
        if ($normalized['name'] !== null) {
            return $normalized['name'];
        }

        $name = trim($this->fetchCompanyNameByCvr((string)$normalized['cvr']));
        if ($name === '') {
            throw new \RuntimeException('Customer name is required to create a new company.', 400);
        }

        return $name;
    }

    protected function resolveCreateEmail(array $normalized, array &$warnings): string
    {
        if ($normalized['email'] !== null) {
            return $normalized['email'];
        }

        $warnings[] = 'No email was provided, defaulted to jb@truckwash.dk for the new e-conomic customer.';
        return 'jb@truckwash.dk';
    }

    protected function searchEconomicCustomersByCvr(string $cvr): array
    {
        $response = (new economic())->customers->customers->search([
            'corporateIdentificationNumber' => $cvr,
        ], [
            'skipPages' => 0,
            'pageSize' => 1000,
        ])->collection ?? [];

        return is_array($response) ? $response : [];
    }

    protected function createEconomicCustomer(array $normalized): object
    {
        $payload = [
            'customerNumber' => (int)$normalized['customer_number'],
            'corporateIdentificationNumber' => (string)$normalized['cvr'],
            'customerGroup' => [
                'customerGroupNumber' => 1,
            ],
            'paymentTerms' => [
                'paymentTermsNumber' => 12,
            ],
            'name' => (string)$normalized['name'],
            'email' => (string)$normalized['email'],
            'phone' => (int)$normalized['phone'],
            'telephoneAndFaxNumber' => (string)$normalized['phone'],
            'mobilePhone' => (string)$normalized['phone'],
            'currency' => 'DKK',
            'vatZone' => [
                'vatZoneNumber' => 1,
            ],
        ];

        if ($normalized['ean'] !== null) {
            $payload['ean'] = (string)$normalized['ean'];
        }

        return (new economic())->customers->customers->create($payload);
    }

    protected function localCustomerNumberExists(int $customerNumber): bool
    {
        $rows = (new users_o())->getFieldsWhere([
            'customer_number' => (string)$customerNumber,
        ], ['id']);

        return count($rows) > 0;
    }

    protected function loadLocalCustomerByNumber(int $customerNumber): ?object
    {
        $customer = (new users_o())->getUserByCustomerNumber($customerNumber);
        return $this->localUserExists($customer) ? $customer : null;
    }

    protected function resolveLocalCustomer(int $customerNumber, bool $localUserExistsBefore, ?object $localUser): object
    {
        if ($localUserExistsBefore && $this->localUserExists($localUser)) {
            return $localUser;
        }

        return $this->bootstrapLocalCustomerOrFail($customerNumber);
    }

    protected function bootstrapLocalCustomerOrFail(int $customerNumber): object
    {
        $customer = (new users_o())->getUserByCustomerNumber($customerNumber);
        if ($this->localUserExists($customer)) {
            return $customer;
        }

        $this->logIssue('CUSTOMER_MASS_IMPORT_LOCAL_BOOTSTRAP_FAILED', [
            'customerNumber' => $customerNumber,
        ]);

        throw new \RuntimeException('Customer was created in e-conomic but could not be imported locally.', 500);
    }

    protected function fetchCompanyNameByCvr(string $cvr): string
    {
        return (string)((new virkdata())->getCompanyInformation($cvr, '', [])->name ?? '');
    }

    protected function logIssue(string $action, array $context): void
    {
        $message = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($message === false) {
            $message = 'Unable to encode customer mass import context';
        }

        (new logs_o())->add('customers', 'global', 0, 0, $action, $message);
    }

    protected function findEconomicCustomerByNumber(array $customers, int $customerNumber): ?object
    {
        foreach ($customers as $customer) {
            if (!is_object($customer)) {
                continue;
            }

            if ($this->extractEconomicCustomerNumber($customer) === $customerNumber) {
                return $customer;
            }
        }

        return null;
    }

    protected function extractEconomicCustomerNumber(object $customer): int
    {
        if (!isset($customer->customerNumber) || !is_numeric($customer->customerNumber)) {
            return 0;
        }

        return (int)$customer->customerNumber;
    }

    protected function resolveExistingCustomerOutcome(bool $localUserExistsBefore, bool $hasAccount): array
    {
        if ($localUserExistsBefore && $hasAccount) {
            return [
                'account_already_exists',
                'Customer already exists locally and already has a login account.',
            ];
        }

        if ($localUserExistsBefore) {
            return [
                'customer_already_exists',
                'Customer already exists locally but does not have a login password yet.',
            ];
        }

        return [
            'imported_existing_customer',
            'Imported an existing e-conomic customer into the local customer database.',
        ];
    }

    protected function resolveCreatedCustomerOutcome(bool $localUserExistsBefore, bool $hasAccount): array
    {
        if ($localUserExistsBefore && $hasAccount) {
            return [
                'economic_customer_created_for_existing_account',
                'Created the e-conomic customer for an existing local login account.',
            ];
        }

        if ($localUserExistsBefore) {
            return [
                'economic_customer_created_for_existing_customer',
                'Created the e-conomic customer for an existing local customer record.',
            ];
        }

        return [
            'created_customer',
            'Created the customer in e-conomic and imported it locally.',
        ];
    }

    protected function buildSuccessResult(
        array $normalized,
        object $customer,
        string $action,
        string $message,
        bool $existingLocalCustomer,
        bool $existingEconomicCustomer,
        bool $createdEconomicCustomer,
        array $warnings
    ): array {
        $customerName = $this->extractLocalUserDisplayName($customer) ?? $normalized['name'];

        return [
            'customer_number' => (int)$normalized['customer_number'],
            'cvr' => (string)$normalized['cvr'],
            'name' => $customerName,
            'email' => $normalized['email'],
            'ean' => $normalized['ean'],
            'action' => $action,
            'message' => $message,
            'user_id' => $this->extractLocalUserId($customer),
            'has_account' => $this->hasLocalAccount($customer),
            'existing_local_customer' => $existingLocalCustomer,
            'existing_economic_customer' => $existingEconomicCustomer,
            'created_economic_customer' => $createdEconomicCustomer,
            'warnings' => array_values(array_filter($warnings, static fn(mixed $warning): bool => is_string($warning) && trim($warning) !== '')),
        ];
    }

    protected function syncLocalCustomer(object $customer, array $normalized, array &$warnings): void
    {
        if (!$customer instanceof users_o || !$customer->exists()) {
            return;
        }

        $name = $normalized['name'] ?? null;
        $email = $normalized['email'] ?? null;
        $phone = $normalized['phone'] ?? null;

        $displayName = trim((string)($customer->display_name->value() ?? ''));
        if ($name !== null && ($displayName === '' || strtolower($displayName) === 'unnamed')) {
            $customer->display_name->set($name);
        }

        if ($email !== null && trim((string)($customer->email->value() ?? '')) === '') {
            try {
                $customer->setEmail($email);
            } catch (\Throwable $throwable) {
                $warnings[] = 'Unable to update local email: ' . $throwable->getMessage();
            }
        }

        if ($phone !== null && empty($customer->phone->value())) {
            try {
                $customer->setPhoneNumber((int)$phone);
            } catch (\Throwable $throwable) {
                $warnings[] = 'Unable to update local phone number: ' . $throwable->getMessage();
            }
        }
    }

    protected function localUserExists(?object $user): bool
    {
        if (!is_object($user)) {
            return false;
        }

        if (method_exists($user, 'exists')) {
            try {
                return (bool)$user->exists();
            } catch (\Throwable) {
                return false;
            }
        }

        return isset($user->id) && is_numeric($user->id) && (int)$user->id > 0;
    }

    protected function hasLocalAccount(?object $user): bool
    {
        if (!$this->localUserExists($user)) {
            return false;
        }

        if (method_exists($user, 'hasPassword')) {
            try {
                return (bool)$user->hasPassword();
            } catch (\Throwable) {
                return false;
            }
        }

        return (bool)($user->has_password ?? false);
    }

    protected function extractLocalUserId(?object $user): ?int
    {
        if (!is_object($user) || !isset($user->id) || !is_numeric($user->id)) {
            return null;
        }

        $userId = (int)$user->id;
        return $userId > 0 ? $userId : null;
    }

    protected function extractLocalUserDisplayName(?object $user): ?string
    {
        if (!is_object($user)) {
            return null;
        }

        if ($user instanceof users_o) {
            $name = trim((string)($user->display_name->value() ?? ''));
            return $name !== '' ? $name : null;
        }

        $name = trim((string)($user->display_name ?? $user->name ?? ''));
        return $name !== '' ? $name : null;
    }
}
