<?php

namespace routes;

use classes\authentication;
use classes\economic;
use classes\email;
use classes\release_manager;
use classes\recaptcha;
use classes\security_policy_service;
use classes\slack;
use classes\totp;
use classes\virkdata;
use classes\webauthn;
use Exception;
use objects\customer_password_reset_keys_o;
use objects\logs_o;
use objects\tokens_o;
use objects\users_o;
use objects\subusers_o;
use objects\passkeys_o;
use traits\route_t;

require_once WD . '/classes/security_policy_service.php';

class authRoute
{
    use route_t;

    private function passkeyChallengePrincipalCacheKey(string $challengeToken): string
    {
        return 'passkey_challenge_principal:' . $challengeToken;
    }

    private function setPasskeyChallengePrincipal(string $challengeToken, string $principalType): void
    {
        if (!defined('redis')) {
            return;
        }
        constant('redis')->setEx($this->passkeyChallengePrincipalCacheKey($challengeToken), $principalType, 5 * 60);
    }

    private function getPasskeyChallengePrincipal(string $challengeToken): string
    {
        if (!defined('redis')) {
            return 'discoverable';
        }
        $principalType = constant('redis')->get($this->passkeyChallengePrincipalCacheKey($challengeToken));
        return is_string($principalType) && in_array($principalType, ['user', 'subuser'], true)
            ? $principalType
            : 'discoverable';
    }

    private function clearPasskeyChallengePrincipal(string $challengeToken): void
    {
        if (!defined('redis')) {
            return;
        }
        constant('redis')->delete($this->passkeyChallengePrincipalCacheKey($challengeToken));
    }

    private function passkeyAllowCredentials(int $userId, bool $isSubuser): array
    {
        if ($userId <= 0) {
            return [];
        }

        $passkeys = new passkeys_o();
        $passkeys->setAdditionalWhereClause(
            '`user_id` = ' . (int)$userId . ' AND `is_subuser` = ' . ($isSubuser ? '1' : '0')
        );
        $list = $passkeys->listObjectsWithPaginationIfSet(function ($o) {
            $transports = null;
            if (isset($o['transports'])) {
                $decoded = json_decode($o['transports'], true);
                $transports = is_array($decoded) ? $decoded : null;
            }
            return [
                'type' => 'public-key',
                'id' => $o['credential_id'] ?? null,
                'transports' => $transports,
            ];
        });

        if (isset($list['items']) && is_array($list['items'])) {
            $list = $list['items'];
        }

        return is_array($list)
            ? array_values(array_filter($list, function ($item) {
                return isset($item['id']) && is_string($item['id']) && strlen($item['id']) > 0;
            }))
            : [];
    }

    public function run(): void
    {
        $this->post('/auth/login', function () {
            // Get the post data
            global $response;
            $this->requireRecaptcha();
            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }
            self::requireParameters(['customer_number', 'password']);
            self::requireType((int)$data['customer_number'], $this->type_int());
            self::requireMinValue((int)$data['customer_number'], 1);
            self::requireType((string)$data['password'], $this->type_string());
            self::requireMinLength('password', 1);
            $data['customer_number'] = (int)$data['customer_number'];
            $data['password'] = (string)$data['password'];
            // Check if the customer number, and password are set
            if (!isset($data['customer_number']) || empty($data['customer_number']) || !is_numeric($data['customer_number']) || $data['customer_number'] < 1) {
                $response->error('Customer number is required', 400);
            }
            if (!isset($data['password']) || empty($data['password']) || strlen($data['password']) < 1) {
                $response->error('Password is required', 400);
            }

            // Use Redis-backed user and property caches to validate credentials with a single user load
            $user = (new users_o())->getUserByCustomerNumber($data['customer_number']);
            if (!$user->exists() || !$user->hasPassword()) {
                (new logs_o())->add('auth', 'global', 1, 0, 'AUTH_FAILURE', 'Customer number: ' . $data['customer_number']);
                $this->observeLoginFailure('customer', $data['customer_number'], ['reason' => 'missing_user_or_password']);
                $response->error('Invalid credentials', 401);
            }

            $isCredentialsValid = false;
            try {
                $isCredentialsValid = $user->passwordMatches($data['password']);
            } catch (Exception $e) {
                $isCredentialsValid = false;
            }

            // Log the incident
            if ($isCredentialsValid) {
                (new logs_o())->add('auth', 'global', 1, 0, 'AUTH_SUCCESS', 'Customer number: ' . $data['customer_number']);
            } else {
                (new logs_o())->add('auth', 'global', 1, 0, 'AUTH_FAILURE', 'Customer number: ' . $data['customer_number']);
                $this->observeLoginFailure('customer', $data['customer_number'], ['reason' => 'invalid_password']);
                $response->error('Invalid credentials', 401);
            }

            // If the credentials are valid, check 2FA and create a token
            if ($user->exists() && $user->isTwoFactorEnabled()) {
                $token = (new authentication())->create_2fa_token($user->id, '2FA_VERIFICATION_USER');
                $response->success(['2fa_required' => true, '2fa_token' => $token]);
            }

            // Create the auth token without reloading the user from DB
            $token = (new authentication())->create_token_by_user_id((int)$user->id);
            // Return the token
            $response->success(['token' => $token]);
        });

        $this->get('/auth/logout', function () {
            // Get the token from the headers
            global $response;
            $token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
            // Remove the Bearer prefix
            $token = str_replace('Bearer ', '', $token);
            // Check if the token is valid
            if (!(new authentication())->validate_token($token)) {
                $response->error('Invalid token', 401);
            }
            // Delete the token
            (new tokens_o())->delete($token);
            // Clear any cached session for this token
            try { redis->clear_auth_session($token); } catch (\Throwable $e) {}
            // Clear any cached subuser session for this token
            try { (new subusers_o())->invalidateSessionToken($token); } catch (\Throwable $e) {}
            // Return a success message
            $response->success(['message' => 'Logged out']);
        });

        $this->get('/auth/session', function () {
            // Get the token from the headers
            global $response;
            $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; // Default to empty string if not set
            // Remove the Bearer prefix
            $token = str_replace('Bearer ', '', $token);
            // Check if the token is valid
            if (!(new authentication())->validate_token($token)) {
                $response->error('Invalid token', 401);
            }

            // Try Redis cache first for session payload
            try {
                $cached = redis->get_auth_session($token);
                if (is_array($cached)) {
                    $cached = $this->appendRuntimeConfig($cached);
                    try {
                        redis->cache_auth_session($token, $cached, 60);
                    } catch (\Throwable $e) {
                        // Best-effort caching only
                    }
                    $response->success($cached);
                }
            } catch (\Throwable $e) {
                // Ignore Redis errors and continue to compute session
            }

            // Get the user object
            $user = (new authentication())->get_user();
            // Check if the user exists
            if (!$user) {
                $response->error('User not found', 400);
            }

            $user_data = $user->includeIncludes(['economicCustomer', 'permissions'])->asArray();
            $user_data['two_factor_enabled'] = $user->isTwoFactorEnabled();
            $user_data = $this->appendRuntimeConfig($user_data);

            // Cache the session payload briefly to reduce DB load on hot paths
            try {
                redis->cache_auth_session($token, $user_data, 60);
            } catch (\Throwable $e) {
                // Best-effort caching only
            }

            // Return the (session) user object
            $response->success($user_data);
        });

        $this->post('/auth/2fa/setup', function () {
            global $response;
            $auth = new authentication();
            $user = $auth->get_user();
            $subuser = $auth->get_subuser();
            if ($user === false && $subuser === false) {
                $response->error('Unauthorized', 401);
            }

            $principal = $subuser ?: $user;
            $totp = new totp();
            $secret = $totp->generateSecret();
            $principal->setTwoFactorSecret($secret);

            if ($principal instanceof subusers_o) {
                $name = $principal->username->value() ?? ('subuser#' . (string)$principal->id);
            } else {
                $name = $principal->customer_number->value();
            }
            $qrCodeUrl = $totp->getQrCodeUrl($secret, $name, 'Truck Wash');

            $response->success([
                'secret' => $secret,
                'qr_code_url' => $qrCodeUrl
            ]);
        });

        $this->post('/auth/2fa/enable', function () {
            global $response;
            $auth = new authentication();
            $user = $auth->get_user();
            $subuser = $auth->get_subuser();
            if ($user === false && $subuser === false) {
                $response->error('Unauthorized', 401);
            }

            self::requireParameters(['code']);
            $code = (string)self::getParameter('code');

            $principal = $subuser ?: $user;
            if ($auth->verify_2fa_code($principal, $code)) {
                $principal->setTwoFactorEnabled(true);
                $response->success(['message' => '2FA enabled successfully']);
            } else {
                $response->error('Invalid 2FA code', 400);
            }
        });

        $this->post('/auth/2fa/disable', function () {
            global $response;
            $auth = new authentication();
            $user = $auth->get_user();
            $subuser = $auth->get_subuser();
            if ($user === false && $subuser === false) {
                $response->error('Unauthorized', 401);
            }

            self::requireParameters(['code']);
            $code = (string)self::getParameter('code');

            $principal = $subuser ?: $user;
            if ($auth->verify_2fa_code($principal, $code)) {
                $principal->setTwoFactorEnabled(false);
                $principal->setTwoFactorSecret(null);
                $response->success(['message' => '2FA disabled successfully']);
            } else {
                $response->error('Invalid 2FA code', 400);
            }
        });

        $this->post('/auth/2fa/verify', function () {
            global $response;
            self::requireParameters(['2fa_token', 'code']);
            $token_str = (string)self::getParameter('2fa_token');
            $code = (string)self::getParameter('code');

            $token_o = new tokens_o();
            try {
                $token = $token_o->getToken($token_str);
            } catch (Exception $e) {
                $response->error('Invalid or expired 2FA token', 401);
            }

            $auth = new authentication();
            $token_type = $token->type->value();
            $user_id = (int)$token->user_id->value();

            if ($token_type === '2FA_VERIFICATION_USER') {
                $user = (new users_o())->getUserById($user_id);
                if ($user->exists() && $auth->verify_2fa_code($user, $code)) {
                    $token_o->delete($token_str);
                    $new_token = $auth->create_employee_token($user_id); // Works for both users and employees
                    $response->success(['token' => $new_token]);
                }
            } elseif ($token_type === '2FA_VERIFICATION_SUBUSER') {
                $subuser = (new subusers_o())->select($user_id);
                if ($auth->verify_2fa_code($subuser, $code)) {
                    $token_o->delete($token_str);
                    $new_token = $subuser->generateSession();
                    $response->success(['session' => $new_token]);
                }
            }

            $response->error('Invalid 2FA code', 400);
        });

        $this->post('/auth/employee/login', function () {
            // Get the post data
            global $response;
            $this->requireRecaptcha();
            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }
            // Check if the employee number, and password are set
            if (!isset($data['user_id'])) {
                $response->error('Employee number is required', 400);
            }
            if (!isset($data['password'])) {
                $response->error('Password is required', 400);
            }
            // Try to log the user in
            $isCredentialsValid = (new authentication())->authenticateEmployee($data['user_id'], $data['password']);
            // Log the incident
            if ($isCredentialsValid) {
                (new logs_o())->add('auth', 'global', 1, 0, 'AUTH_SUCCESS', 'Employee number: ' . $data['user_id']);
            } else {
                (new logs_o())->add('auth', 'global', 1, 0, 'AUTH_FAILURE', 'Employee number: ' . $data['user_id']);
                $this->observeLoginFailure('employee', (string)$data['user_id'], ['reason' => 'invalid_credentials']);
                $response->error('Invalid credentials', 401);
            }

            $user = (new users_o())->getUserById($data['user_id']);
            if ($user->exists() && $user->isTwoFactorEnabled()) {
                $token = (new authentication())->create_2fa_token($user->id, '2FA_VERIFICATION_USER');
                $response->success(['2fa_required' => true, '2fa_token' => $token]);
            }

            // If the credentials are valid, create a token
            $token = (new authentication())->create_employee_token($data['user_id']);
            // Return the token
            $response->success(['token' => $token]);
        });

        $this->get('/auth/reCAPTCHA/public', function () {
            // Check if the user:
            // 1. Is rate limited (future feature)
            // 2. Is required to solve a reCAPTCHA
            global $response;
            $recaptcha = (new recaptcha())->getPublicConfig();
            $response->success([
                'rate_limit' => [
                    'enabled' => false,
                    'limit' => 0,
                    'remaining' => 0,
                    'reset' => 0,
                    'warning' => null
                ],
                'recaptcha' => $recaptcha
            ]);
        });

        $this->post('/auth/register/cvr', function () {
            // Get the post data
            global $response;
            $this->requireRecaptcha();
            /**
             * {
             * "cvr": "44794780",
             * "companyPhone": 21754690,
             * "invoiceEmail": "mikkel@truckwash.dk",
             * "contactEmail": "mikkel@truckwash.dk",
             * "contactPhone": 21754690,
             * "searchResult": {
             * "vat": 41004355,
             * "status": "Normal",
             * "name": "Truckwash ApS",
             * "address": "Letland Alle 2",
             * "zipcode": 2630,
             * "city": "Taastrup",
             * "protected": true,
             * "phone": "21754690",
             * "website": null,
             * "email": "mikkel@truckwash.dk",
             * "fax": null,
             * "startdate": "2019-12-11",
             * "enddate": null,
             * "employees": 14,
             * "industrycode": 953190,
             * "industrydesc": "Reparation og vedligeholdelse af motorkøretøjer i.a.n.",
             * "companytype": "APS",
             * "companydesc": "Anpartsselskab",
             * "owners": [
             * "DELOITTE STATSAUTORISERET REVISIONSPARTNERSELSKAB",
             * "MBL Revision I/S",
             * "WASH GROUP A/S"
             * ]
             * }
             * }
             */
            /**
             * Parameters:
             */
            self::requireParameters(['cvr', 'companyPhone', 'invoiceEmail', 'contactEmail', 'contactPhone']);
            $cvr = self::getParameter('cvr');
            $companyPhone = (int)self::getParameter('companyPhone');
            $invoiceEmail = self::getParameter('invoiceEmail');
            $contactEmail = self::getParameter('contactEmail');
            $contactPhone = (int)self::getParameter('contactPhone');
            $contactName = self::getParameter('contactName');
            $ean = null;
            /**
             * Validate
             */
            self::requireType($cvr, $this->type_string());
            self::requireMinLength('cvr', 8);
            self::requireMaxLength('cvr', 20);
            self::requireType($companyPhone, $this->type_int());
            self::requireMinValue($companyPhone, 10000000);
            self::requireMaxValue($companyPhone, 9999999999);
            if (self::isParametersSet(['invoiceEmail']) && !is_null($invoiceEmail)) {
                self::requireType($invoiceEmail, $this->type_string());
                self::requireMinLength('invoiceEmail', 5);
                self::requireMaxLength('invoiceEmail', 255);
            }
            if (self::isParametersSet(['contactEmail']) && !is_null($contactEmail)) {
                self::requireType($contactEmail, $this->type_string());
                self::requireMinLength('contactEmail', 5);
                self::requireMaxLength('contactEmail', 255);
            }
            /**
             * If the contact phone is set, validate it
             */
            if (self::isParametersSet(['contactPhone']) && !empty($contactPhone)) {
                self::requireType($contactPhone, $this->type_int());
                self::requireMinValue($contactPhone, 10000000);
                self::requireMaxValue($contactPhone, 9999999999);
            }
            if (self::isParametersSet(['ean'])) {
                try {
                    $ean = economic::normalizeCustomerEan(self::getParameter('ean'));
                } catch (\InvalidArgumentException $exception) {
                    $response->error($exception->getMessage(), 400);
                }
            }

            /**
             * If the contact phone is empty, default to company phone
             */
            if (empty($contactPhone)) {
                $contactPhone = $companyPhone;
            }
            /**
             * If the emails are empty, default to jb@truckwash.dk
             */
            if (empty($invoiceEmail)) {
                $invoiceEmail = 'jb@truckwash.dk';
            }
            if (empty($contactEmail)) {
                $contactEmail = 'jb@truckwash.dk';
            }

            /**
             * Check if the cvr already exists
             */
            $economic = new economic();
            $economic_response = $this->searchEconomicCustomersByCvr($economic, (string)$cvr);

            $localUserExists = $this->localCustomerNumberExists($companyPhone);
            $matchingEconomicCustomer = $this->findEconomicCustomerByNumber($economic_response, $companyPhone);

            if ($matchingEconomicCustomer !== null) {
                if ($localUserExists) {
                    $response->error('Company phone number already registered', 400);
                }

                $this->bootstrapLocalCustomerOrFail($companyPhone);
                $this->sendRegistrationWelcomeEmails($companyPhone, (string)$invoiceEmail);
                $response->success($matchingEconomicCustomer, 200);
            }

            if (count($economic_response) > 0) {
                $existingEconomicCustomerNumber = $this->extractEconomicCustomerNumber($economic_response[0]);
                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_CONFLICT', [
                    'phase' => 'search',
                    'cvr' => (string)$cvr,
                    'requestedCustomerNumber' => $companyPhone,
                    'existingCustomerNumber' => $existingEconomicCustomerNumber,
                ]);
                $response->error(
                    '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
                );
            }

            // Get the CVR company information used for the e-conomic customer payload.
            $companyInformation = null;
            try {
                $companyInformation = (new virkdata())->getCompanyInformation((string)$cvr, '', []);
            } catch (Exception $exception) {
                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_LOOKUP_FAILED', [
                    'phase' => 'cvr_lookup',
                    'cvr' => (string)$cvr,
                    'requestedCustomerNumber' => $companyPhone,
                    'message' => $exception->getMessage(),
                ]);
                $response->error('CVR could not be verified. Please check the CVR number and try again.', 400);
            }

            $name = trim((string)($companyInformation->name ?? ''));
            if ($name === '') {
                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_LOOKUP_INVALID_RESPONSE', [
                    'phase' => 'cvr_lookup',
                    'cvr' => (string)$cvr,
                    'requestedCustomerNumber' => $companyPhone,
                ]);
                $response->error('CVR could not be verified. Please check the CVR number and try again.', 400);
            }

            if ($localUserExists) {
                $response->error('Company phone number already registered', 400);
            }

            try {
                $result = $economic->createCustomer(
                    (int)$companyPhone,
                    $name,
                    (int)$cvr,
                    (string)$invoiceEmail,
                    (int)$companyPhone,
                    (int)$contactPhone,
                    $companyInformation,
                    $ean,
                );
            } catch (Exception $exception) {
                $recoveredCustomer = $this->recoverRegistrationAfterCreateFailure(
                    $economic,
                    (string)$cvr,
                    $companyPhone,
                    (string)$invoiceEmail
                );

                if ($recoveredCustomer !== null) {
                    $response->success($recoveredCustomer, 200);
                }

                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_CREATE_FAILED', [
                    'phase' => 'create',
                    'cvr' => (string)$cvr,
                    'requestedCustomerNumber' => $companyPhone,
                    'message' => $exception->getMessage(),
                ]);
                $response->error('Failed to create customer in e-conomic.', 502);
            }

            if (!isset($result->customerNumber) || !is_numeric($result->customerNumber)) {
                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_INVALID_CREATE_RESPONSE', [
                    'phase' => 'create',
                    'cvr' => (string)$cvr,
                    'requestedCustomerNumber' => $companyPhone,
                    'response' => $result,
                ]);
                $response->error('Customer creation did not return a valid customer number.', 500);
            }

            $createdCustomerNumber = (int)$result->customerNumber;
            if ($createdCustomerNumber !== $companyPhone) {
                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_CONFLICT', [
                    'phase' => 'create',
                    'cvr' => (string)$cvr,
                    'requestedCustomerNumber' => $companyPhone,
                    'createdCustomerNumber' => $createdCustomerNumber,
                    'logId' => isset($result->logId) ? (string)$result->logId : null,
                    'message' => isset($result->message) ? (string)$result->message : null,
                ]);
                $response->error(
                    'E-conomic created the customer under customer number '
                    . $createdCustomerNumber
                    . ' instead of the submitted phone number '
                    . $companyPhone
                    . '. Manual cleanup or reassignment is required before retrying.',
                    409
                );
            }

            $this->bootstrapLocalCustomerOrFail($companyPhone, $result);
            $this->sendRegistrationWelcomeEmails($companyPhone, (string)$invoiceEmail);
            $response->success($result, 201);
        });

        $this->post('/auth/password-reset/request', function () {
            global $response;
            $this->requireRecaptcha();
            self::requireParameters(['customer_number']);
            $customer_number = (int)self::getParameter('customer_number');

            $user = (new users_o())->getUserByCustomerNumber($customer_number);
            if (!$user->exists()) {
                // For security reasons, don't reveal if the user exists
                $response->success(['message' => 'If the customer exists, a password reset email has been sent.']);
            }

            $email_address = $user->email->value();
            if (empty($email_address)) {
                // If no email is set, we can't send the reset email
                $response->error('Der er ingen email-addresse tilknyttet denne konto, ring venligst på +45 43 71 78 86 for konto gendannelse.', 400);
            }

            // Generate token
            $token = customer_password_reset_keys_o::generateToken();

            // Save token
            $reset_key_o = new customer_password_reset_keys_o();
            $reset_key_o->add([
                'customer_id' => $customer_number,
                'token' => $token,
                'note' => 'Requested via API'
            ]);

            // Send email
            $email = new email();
            $reset_link = "https://truckwash.io/auth/password-reset/" . $token;

            $subject = 'Adgangskode nulstilling';
            $valid_hours = (int)(customer_password_reset_keys_o::TOKEN_EXPIRY_SECONDS / 3600);
            $message = "Du har anmodet om at nulstille din adgangskode. Klik på linket herunder for at fortsætte:<br><br><a href='$reset_link'>$reset_link</a><br><br>Linket er gyldigt i $valid_hours timer.";

            try {
                $email->sendEmail($email_address, $user->display_name->value() ?? 'Kunde', $subject, $message, null);
                $response->success(['message' => 'If the customer exists, a password reset email has been sent.']);
            } catch (Exception $e) {
                $response->error('Failed to send email: ' . $e->getMessage(), 500);
            }
        });

        $this->get('/auth/password-reset/validate', function () {
            global $response;
            self::requireParameters(['token']);
            $token = self::getParameter('token');

            $reset_key_o = new customer_password_reset_keys_o();
            $found_key = $reset_key_o->findValidByToken($token);

            if ($found_key === null) {
                $response->error('Invalid or expired token', 404);
            }

            $response->success([
                'valid' => true,
                'customer_id' => $found_key->customer_id->value()
            ]);
        });

        $this->post('/auth/password-reset/set', function () {
            global $response;
            $this->requireRecaptcha();
            self::requireParameters(['token', 'password']);
            $token = self::getParameter('token');
            $password = self::getParameter('password');

            $reset_key_o = new customer_password_reset_keys_o();
            $found_key = $reset_key_o->findValidByToken($token);

            if ($found_key === null) {
                $response->error('Invalid or expired token', 404);
            }

            try {
                $found_key->setPassword($password);
                $response->success(['message' => 'Password updated successfully']);
            } catch (Exception $e) {
                $response->error($e->getMessage(), 400);
            }
        });

        $this->post('/auth/passkey/challenge', function () {
            global $response;
            $this->requireRecaptcha();

            $principal_type = strtolower(trim((string)(self::getParameter('principal_type') ?? self::getParameter('user_type') ?? '')));
            if ($principal_type === '') {
                $principal_type = self::getParameter('customer_number') !== null ? 'user' : 'discoverable';
            }
            if (!in_array($principal_type, ['user', 'subuser', 'discoverable'], true)) {
                $response->error('Invalid principal_type', 400);
            }

            $customer_number = self::getParameter('customer_number');
            $user_id = 0;
            $allowCredentials = [];

            if ($principal_type === 'subuser') {
                $subuser = null;
                if (self::isParametersSet(['subuser_id'])) {
                    $subuser_id = (int)self::getParameter('subuser_id');
                    self::requireType($subuser_id, $this->type_int());
                    self::requireMinValue($subuser_id, 1);
                    $candidate = (new subusers_o())->select($subuser_id);
                    if ($candidate->exists()) {
                        $candidate->getObjectProperties();
                        $subuser = $candidate;
                    }
                } elseif (self::isParametersSet(['username'])) {
                    $username = (string)self::getParameter('username');
                    self::requireType($username, $this->type_string());
                    self::requireMinLength('username', 3);
                    self::requireMaxLength('username', 255);
                    $subuser = (new subusers_o())->getSubuserByUsername($username);
                } elseif (self::isParametersSet(['phone_country_code', 'phone'])) {
                    $phone_country_code = (int)self::getParameter('phone_country_code');
                    $phone = (int)self::getParameter('phone');
                    self::requireType($phone_country_code, $this->type_int());
                    self::requireMinLength('phone_country_code', 1);
                    self::requireMaxLength('phone_country_code', 3);
                    self::requireType($phone, $this->type_int());
                    self::requireMinLength('phone', 4);
                    self::requireMaxLength('phone', 15);
                    $subuser = (new subusers_o())->getSubuserByPhone($phone_country_code, $phone);
                }

                if ($subuser !== null) {
                    $user_id = (int)$subuser->id;
                    $allowCredentials = $this->passkeyAllowCredentials($user_id, true);
                }
            } elseif ($customer_number !== null) {
                $principal_type = 'user';
                $customer_number = (int)$customer_number;
                self::requireType($customer_number, $this->type_int());
                self::requireMinValue($customer_number, 1);

                // Attempt to locate user; do not reveal existence in response
                $user = (new users_o())->getUserByCustomerNumber($customer_number);

                if ($user->exists()) {
                    $user_id = (int)$user->id;
                    $allowCredentials = $this->passkeyAllowCredentials($user_id, false);
                }
            }

            // Generate a challenge (Base64URL without padding)
            $challenge_token = bin2hex(random_bytes(32));
            $rawChallenge = hex2bin($challenge_token);
            $challenge = rtrim(strtr(base64_encode($rawChallenge), '+/', '-_'), '=');

            // rpId for passkeys
            $originHost = parse_url((string)($_SERVER['HTTP_ORIGIN'] ?? ''), PHP_URL_HOST);
            $httpHost = isset($_SERVER['HTTP_HOST']) ? explode(':', (string)$_SERVER['HTTP_HOST'])[0] : null;
            $serverName = $_SERVER['SERVER_NAME'] ?? null;
            $rpId = $originHost ?: $httpHost ?: $serverName ?: 'truckwash.io';

            // Create an ephemeral token to bind the challenge to the (potential) user
            (new tokens_o())->create($user_id, $challenge_token, 'PASSKEY_CHALLENGE');
            $this->setPasskeyChallengePrincipal(
                $challenge_token,
                $principal_type === 'subuser' ? 'subuser' : ($principal_type === 'user' ? 'user' : 'discoverable')
            );

            $logDetails = $customer_number ? 'Issued passkey challenge for customer ' . $customer_number : 'Issued passkey challenge (discoverable)';
            (new logs_o())->add('auth', 'global', 1, $user_id, 'AUTH_PASSKEY_CHALLENGE', $logDetails);

            $response->success([
                'challenge_token' => $challenge_token,
                'publicKey' => [
                    'challenge' => $challenge,
                    'rpId' => $rpId,
                    'timeout' => 60000,
                    'userVerification' => 'preferred',
                    'allowCredentials' => $allowCredentials
                ]
            ]);
        });

        $this->post('/auth/passkey/verify', function () {
            global $response;
            $this->requireRecaptcha();

            // Parse JSON body
            $data = json_decode(file_get_contents('php://input'), true);
            if (!is_array($data)) {
                $data = [];
            }

            if (!isset($data['challenge_token']) || !is_string($data['challenge_token']) || strlen($data['challenge_token']) < 10) {
                $response->error('Invalid or missing challenge_token', 400);
            }
            if (!isset($data['credential'])) {
                $response->error('Missing credential', 400);
            }

            $challenge_token = (string)$data['challenge_token'];
            $credentialPayload = $data['credential'];
            $credentialJson = is_string($credentialPayload) ? $credentialPayload : json_encode($credentialPayload, JSON_UNESCAPED_SLASHES);
            if (!is_string($credentialJson) || $credentialJson === false) {
                $response->error('Invalid credential payload', 400);
            }

            // Load and validate the ephemeral challenge token
            $token_o = new tokens_o();
            try {
                $token = $token_o->getToken($challenge_token);
            } catch (Exception $e) {
                $response->error('Invalid or expired challenge', 401);
            }
            $token_type = $token->type->value();
            if ($token_type !== 'PASSKEY_CHALLENGE') {
                $response->error('Invalid token type', 401);
            }
            $challengePrincipalType = $this->getPasskeyChallengePrincipal($challenge_token);

            // Determine rpId/host
            $host = parse_url((string)($_SERVER['HTTP_ORIGIN'] ?? ''), PHP_URL_HOST) ?: ($_SERVER['SERVER_NAME'] ?? 'localhost');

            // Extract credential id
            $credentialArr = is_array($credentialPayload) ? $credentialPayload : json_decode($credentialJson, true);
            $credentialId = $credentialArr['id'] ?? $credentialArr['rawId'] ?? null;
            if (!is_string($credentialId) || $credentialId === '') {
                $response->error('Invalid credential id', 400);
            }

            // Find corresponding stored passkey
            $user_id_hint = (int)$token->user_id->value();
            $passkeyRepo = new passkeys_o();
            $passkey = $passkeyRepo->findByCredentialId($credentialId, $user_id_hint > 0 ? $user_id_hint : null);
            if ($passkey === null) {
                // As a fallback for discoverable credentials, try without user hint
                $passkey = (new passkeys_o())->findByCredentialId($credentialId, null);
            }
            if ($passkey === null) {
                (new logs_o())->add('auth', 'global', 1, $user_id_hint, 'AUTH_PASSKEY_VERIFY_FAILURE', 'Unknown credential');
                $this->observeLoginFailure('passkey', $credentialId, ['reason' => 'unknown_credential', 'user_id_hint' => $user_id_hint]);
                $response->error('Invalid credential', 404);
            }

            // Verify assertion using the WebAuthn library
            $wa = new webauthn();
            $ok = $wa->verifyAssertion($credentialJson, $challenge_token, $passkey, $host);
            if (!$ok) {
                (new logs_o())->add('auth', 'global', 1, (int)$passkey->user_id->value(), 'AUTH_PASSKEY_VERIFY_FAILURE', 'Assertion verification failed');
                $this->observeLoginFailure('passkey', $credentialId, ['reason' => 'assertion_failed', 'user_id_hint' => $user_id_hint]);
                $response->error('Invalid passkey assertion', 401);
            }

            // Success → issue session token accordingly and delete the challenge token
            $issued_to_user_id = (int)$passkey->user_id->value();
            $is_subuser = (bool)$passkey->is_subuser->value();
            if (
                ($challengePrincipalType === 'subuser' && !$is_subuser)
                || ($challengePrincipalType === 'user' && $is_subuser)
            ) {
                (new logs_o())->add('auth', 'global', 1, $user_id_hint, 'AUTH_PASSKEY_VERIFY_FAILURE', 'Credential principal mismatch');
                $this->observeLoginFailure('passkey', $credentialId, ['reason' => 'principal_mismatch', 'user_id_hint' => $user_id_hint]);
                $token_o->delete($challenge_token);
                $this->clearPasskeyChallengePrincipal($challenge_token);
                $response->error('Invalid credential', 401);
            }
            $token_o->delete($challenge_token);
            $this->clearPasskeyChallengePrincipal($challenge_token);

            (new logs_o())->add('auth', 'global', 1, $issued_to_user_id, 'AUTH_PASSKEY_VERIFY_SUCCESS', 'Passkey assertion accepted');

            if ($is_subuser) {
                $subuser = (new subusers_o())->select($issued_to_user_id);
                $session = $subuser->generateSession();
                $response->success(['session' => $session]);
            } else {
                // For customers, user_id stores the customer number
                $auth = new authentication();
                $jwt = $auth->create_token_by_user_id($issued_to_user_id);
                $response->success(['token' => $jwt]);
            }
        });

    }

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

        return count($rows) > 0;
    }

    private function searchEconomicCustomersByCvr(economic $economic, string $cvr): array
    {
        $economic_response = ($economic->customers->customers->search([
            'corporateIdentificationNumber' => $cvr,
        ], [
            'skipPages' => 0,
            'pageSize' => 1,
        ])->collection);

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

    private function recoverRegistrationAfterCreateFailure(
        economic $economic,
        string $cvr,
        int $customerNumber,
        string $invoiceEmail
    ): ?object {
        // The upstream POST can commit before the client receives a validation/transport error.
        // Re-read by CVR and only recover when e-conomic confirms the requested customer number.
        try {
            $economic_response = $this->searchEconomicCustomersByCvr($economic, $cvr);
        } catch (Exception $searchException) {
            $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_CREATE_RECOVERY_SEARCH_FAILED', [
                'phase' => 'create_recovery',
                'cvr' => $cvr,
                'requestedCustomerNumber' => $customerNumber,
                'message' => $searchException->getMessage(),
            ]);
            return null;
        }

        $matchingEconomicCustomer = $this->findEconomicCustomerByNumber($economic_response, $customerNumber);
        if ($matchingEconomicCustomer === null || $this->localCustomerNumberExists($customerNumber)) {
            return null;
        }

        $this->bootstrapLocalCustomerOrFail($customerNumber, $matchingEconomicCustomer);
        $this->sendRegistrationWelcomeEmails($customerNumber, $invoiceEmail);

        return $matchingEconomicCustomer;
    }

    private 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;
    }

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

        return (int)$customer->customerNumber;
    }

    /**
     * @throws Exception
     */
    private function bootstrapLocalCustomerOrFail(int $customerNumber, ?object $economicCustomer = null): users_o
    {
        global $response;

        $customer = new users_o();
        try {
            $customer = $customer->getUserByCustomerNumber($customerNumber);
            if (method_exists($customer, 'exists') && $customer->exists()) {
                return $customer;
            }
        } catch (Exception $exception) {
            $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_LOCAL_BOOTSTRAP_LOOKUP_FAILED', [
                'customerNumber' => $customerNumber,
                'message' => $exception->getMessage(),
            ]);
        }

        if (
            $economicCustomer !== null
            && $this->extractEconomicCustomerNumber($economicCustomer) === $customerNumber
            && method_exists($customer, 'importCustomerFromEconomicCustomerData')
        ) {
            try {
                $importedCustomer = $customer->importCustomerFromEconomicCustomerData($economicCustomer);
                if (is_object($importedCustomer) && method_exists($importedCustomer, 'exists') && $importedCustomer->exists()) {
                    return $importedCustomer;
                }
            } catch (Exception $exception) {
                $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_LOCAL_SNAPSHOT_BOOTSTRAP_FAILED', [
                    'customerNumber' => $customerNumber,
                    'message' => $exception->getMessage(),
                ]);
            }
        }

        $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_LOCAL_BOOTSTRAP_FAILED', [
            'customerNumber' => $customerNumber,
        ]);
        $response->error('Customer was created in e-conomic but could not be imported locally.', 500);
    }

    /**
     * @throws Exception
     */
    private function sendRegistrationWelcomeEmails(int $customerNumber, string $invoiceEmail): void
    {
        $email = new email();
        $jimmyEmail = 'jm@truckwash.dk';
        //$infoEmail = "info@truckwash.dk";
        //$email->sendWelcomeEmailToCustomer($customerNumber, (string)$infoEmail); - Disabled, requested by Christian.
        $email->sendWelcomeEmailToCustomer($customerNumber, $jimmyEmail);
        $email->sendWelcomeEmailToCustomer($customerNumber, $invoiceEmail);

        try {
            $email->sendNewCustomerRegistrationNotifications($customerNumber);
        } catch (Exception $exception) {
            $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_SUPERUSER_NOTIFICATION_FAILED', [
                'customerNumber' => $customerNumber,
                'message' => $exception->getMessage(),
            ]);
        }

        try {
            (new slack())->send_customer_registration_notification($customerNumber);
        } catch (Exception $exception) {
            $this->logRegisterCvrIssue('AUTH_REGISTER_CVR_SLACK_NOTIFICATION_FAILED', [
                'customerNumber' => $customerNumber,
                'message' => $exception->getMessage(),
            ]);
        }
    }

    private function logRegisterCvrIssue(string $action, array $context): void
    {
        $message = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($message === false) {
            $message = 'Unable to encode register/cvr context';
        }

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

    private function observeLoginFailure(string $principalType, string|int $identifier, array $metadata = []): void
    {
        try {
            (new security_policy_service())->observeLoginFailure($principalType, $identifier, $metadata);
        } catch (\Throwable) {
            // Security observation must not change authentication responses.
        }
    }

    private function appendRuntimeConfig(array $payload): array
    {
        $payload['runtime_config'] = array_replace_recursive(
            is_array($payload['runtime_config'] ?? null) ? $payload['runtime_config'] : [],
            [
                'economic' => [
                    'transaction_draft_customer_number' => (new economic())->getTransactionDraftCustomerNumber(),
                    'default_distribution_department_id' => (new economic())->getDefaultDistributionDepartmentId(),
                ],
                'release' => (new release_manager())->runtimeForPayload($payload, $this->getParametersAsArray()),
            ]
        );

        return $payload;
    }
}
