<?php

namespace classes;

use Exception;
use objects\logs_o;
use objects\subusers_o;

class subuser_contact_verification_service
{
    public const CHANNEL_EMAIL = 'email';
    public const CHANNEL_PHONE = 'phone';
    public const CODE_TTL_SECONDS = 600;
    public const RESEND_COOLDOWN_SECONDS = 60;
    public const MAX_ATTEMPTS = 5;

    private ?object $redis;
    private $codeGenerator;
    private $timeProvider;
    private $smsSender;
    private $emailSender;

    public function __construct(
        ?object $redis = null,
        ?callable $codeGenerator = null,
        ?callable $timeProvider = null,
        ?callable $smsSender = null,
        ?callable $emailSender = null
    ) {
        $this->redis = $redis ?? (defined('redis') ? constant('redis') : null);
        $this->codeGenerator = $codeGenerator;
        $this->timeProvider = $timeProvider;
        $this->smsSender = $smsSender;
        $this->emailSender = $emailSender;
    }

    /**
     * @return array{state:string,email:array<string,mixed>,phone:array<string,mixed>}
     */
    public function status(subusers_o $subuser): array
    {
        $email = $this->emailDestination($subuser);
        $phone = $this->phoneDestination($subuser);
        $emailVerifiedAt = $this->verifiedAtValue($subuser, self::CHANNEL_EMAIL);
        $phoneVerifiedAt = $this->verifiedAtValue($subuser, self::CHANNEL_PHONE);

        $emailStatus = [
            'channel' => self::CHANNEL_EMAIL,
            'value' => $email,
            'masked_value' => $email !== null ? $this->maskEmail($email) : null,
            'available' => $email !== null,
            'verified' => $email !== null && $emailVerifiedAt !== null,
            'verified_at' => $emailVerifiedAt,
        ];
        $phoneStatus = [
            'channel' => self::CHANNEL_PHONE,
            'country_code' => $subuser->phone_country_code->value() !== null ? (int)$subuser->phone_country_code->value() : null,
            'phone' => $subuser->phone->value() !== null ? (int)$subuser->phone->value() : null,
            'value' => $phone,
            'masked_value' => $phone !== null ? $this->maskPhone($phone) : null,
            'available' => $phone !== null,
            'verified' => $phone !== null && $phoneVerifiedAt !== null,
            'verified_at' => $phoneVerifiedAt,
        ];

        return [
            'state' => $this->verificationState($emailStatus, $phoneStatus),
            'email' => $emailStatus,
            'phone' => $phoneStatus,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function sendCode(subusers_o $subuser, string $channel, array $actor = []): array
    {
        $channel = $this->normalizeChannel($channel);
        $destination = $this->destinationFor($subuser, $channel);
        if ($destination === null) {
            return $this->delivery($channel, 'missing_destination', 'No contact value is available for verification.');
        }

        if ($this->redis === null) {
            return $this->delivery($channel, 'unavailable', 'Verification delivery is not available.');
        }

        $now = $this->now();
        $existing = $this->readChallenge($subuser, $channel);
        if ($existing !== null) {
            $sentAt = (int)($existing['sent_at'] ?? 0);
            $retryAfter = self::RESEND_COOLDOWN_SECONDS - ($now - $sentAt);
            if ($retryAfter > 0) {
                return $this->delivery($channel, 'throttled', 'Please wait before requesting another code.', [
                    'retry_after' => $retryAfter,
                ]);
            }
        }

        $code = $this->generateCode();
        $destinationKey = $this->destinationKey($subuser, $channel);
        $nonce = bin2hex(random_bytes(16));
        $payload = [
            'hash' => $this->hashCode($code, $destinationKey, $nonce),
            'nonce' => $nonce,
            'destination' => $destinationKey,
            'attempts' => 0,
            'sent_at' => $now,
            'expires_at' => $now + self::CODE_TTL_SECONDS,
        ];

        try {
            $this->deliverCode($subuser, $channel, $destination, $code);
            $this->writeChallenge($subuser, $channel, $payload);
            $this->logEvent('SUBUSER_CONTACT_VERIFICATION_SENT', $subuser, $channel, $actor);
        } catch (Exception $exception) {
            $this->logEvent('SUBUSER_CONTACT_VERIFICATION_FAILED', $subuser, $channel, $actor);
            return $this->delivery($channel, 'failed', 'Verification delivery failed.');
        }

        return $this->delivery($channel, 'sent', 'Verification code sent.', [
            'masked_destination' => $channel === self::CHANNEL_EMAIL
                ? $this->maskEmail($destination)
                : $this->maskPhone($destination),
            'expires_in' => self::CODE_TTL_SECONDS,
        ]);
    }

    /**
     * @return array<string,mixed>
     */
    public function verifyCode(subusers_o $subuser, string $channel, string $code, array $actor = []): array
    {
        $channel = $this->normalizeChannel($channel);
        $code = trim($code);
        if (!preg_match('/^[0-9]{6}$/', $code)) {
            return $this->verificationResult($channel, 'invalid_code', 'Invalid verification code.');
        }

        $challenge = $this->readChallenge($subuser, $channel);
        if ($challenge === null || (int)($challenge['expires_at'] ?? 0) < $this->now()) {
            $this->deleteChallenge($subuser, $channel);
            return $this->verificationResult($channel, 'expired', 'Verification code expired.');
        }

        if (($challenge['destination'] ?? null) !== $this->destinationKey($subuser, $channel)) {
            $this->deleteChallenge($subuser, $channel);
            return $this->verificationResult($channel, 'destination_changed', 'Contact value changed. Request a new code.');
        }

        $attempts = (int)($challenge['attempts'] ?? 0);
        if ($attempts >= self::MAX_ATTEMPTS) {
            $this->deleteChallenge($subuser, $channel);
            return $this->verificationResult($channel, 'too_many_attempts', 'Too many verification attempts.');
        }

        $expectedHash = (string)($challenge['hash'] ?? '');
        $nonce = (string)($challenge['nonce'] ?? '');
        if (!hash_equals($expectedHash, $this->hashCode($code, (string)$challenge['destination'], $nonce))) {
            $nextAttempts = $attempts + 1;
            if ($nextAttempts >= self::MAX_ATTEMPTS) {
                $this->deleteChallenge($subuser, $channel);
                return $this->verificationResult($channel, 'too_many_attempts', 'Too many verification attempts.');
            }
            $challenge['attempts'] = $nextAttempts;
            $remainingTtl = max(1, (int)($challenge['expires_at'] ?? $this->now()) - $this->now());
            $this->writeChallenge($subuser, $channel, $challenge, $remainingTtl);
            return $this->verificationResult($channel, 'invalid_code', 'Invalid verification code.');
        }

        $this->markVerified($subuser, $channel);
        $this->deleteChallenge($subuser, $channel);
        $this->logEvent('SUBUSER_CONTACT_VERIFICATION_VERIFIED', $subuser, $channel, $actor);

        return $this->verificationResult($channel, 'verified', 'Contact value verified.', [
            'verified_at' => $this->verifiedAtValue($subuser, $channel),
        ]);
    }

    /**
     * @return array<string,mixed>
     */
    public function setVerificationState(subusers_o $subuser, string $channel, bool $verified, array $actor = []): array
    {
        $channel = $this->normalizeChannel($channel);
        if ($verified && $this->destinationFor($subuser, $channel) === null) {
            return $this->verificationResult($channel, 'missing_destination', 'No contact value is available for verification.');
        }

        if ($verified) {
            $this->markVerified($subuser, $channel);
        } else {
            $this->markUnverified($subuser, $channel);
        }

        $this->deleteChallenge($subuser, $channel);
        $this->logEvent(
            $verified
                ? 'SUBUSER_CONTACT_VERIFICATION_MARKED_VERIFIED'
                : 'SUBUSER_CONTACT_VERIFICATION_MARKED_UNVERIFIED',
            $subuser,
            $channel,
            $actor
        );

        return $this->verificationResult($channel, $verified ? 'verified' : 'unverified', 'Contact verification state updated.', [
            'verified_at' => $this->verifiedAtValue($subuser, $channel),
        ]);
    }

    /**
     * @param array<string,mixed> $updates
     * @return array<string,mixed>
     */
    public function clearVerificationForChangedContacts(subusers_o $subuser, array $updates): array
    {
        if (array_key_exists('email', $updates)) {
            $current = $this->normalizeNullableString($subuser->email->value());
            $next = $this->normalizeNullableString($updates['email']);
            if ($current !== $next) {
                $updates['email_verified_at'] = null;
                $this->deleteChallenge($subuser, self::CHANNEL_EMAIL);
            }
        }

        $phoneChanged = false;
        if (array_key_exists('phone_country_code', $updates)) {
            $phoneChanged = (int)$subuser->phone_country_code->value() !== (int)$updates['phone_country_code'];
        }
        if (array_key_exists('phone', $updates)) {
            $phoneChanged = $phoneChanged || (int)$subuser->phone->value() !== (int)$updates['phone'];
        }
        if ($phoneChanged) {
            $updates['phone_verified_at'] = null;
            $this->deleteChallenge($subuser, self::CHANNEL_PHONE);
        }

        return $updates;
    }

    public function normalizeChannel(string $channel): string
    {
        $channel = strtolower(trim($channel));
        if (!in_array($channel, [self::CHANNEL_EMAIL, self::CHANNEL_PHONE], true)) {
            throw new Exception('Invalid verification channel');
        }

        return $channel;
    }

    private function now(): int
    {
        if ($this->timeProvider !== null) {
            return (int)call_user_func($this->timeProvider);
        }

        return time();
    }

    private function generateCode(): string
    {
        if ($this->codeGenerator !== null) {
            $code = (string)call_user_func($this->codeGenerator);
            if (preg_match('/^[0-9]{6}$/', $code)) {
                return $code;
            }
        }

        return (string)random_int(100000, 999999);
    }

    private function hashCode(string $code, string $destination, string $nonce): string
    {
        $secret = (string)(getenv('APP_KEY') ?: getenv('JWT_SECRET') ?: __FILE__);
        return hash('sha256', $secret . ':' . $nonce . ':' . $destination . ':' . $code);
    }

    private function destinationFor(subusers_o $subuser, string $channel): ?string
    {
        return $channel === self::CHANNEL_EMAIL
            ? $this->emailDestination($subuser)
            : $this->phoneDestination($subuser);
    }

    private function emailDestination(subusers_o $subuser): ?string
    {
        $email = $this->normalizeNullableString($subuser->email->value());
        return $email !== null && filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null;
    }

    private function phoneDestination(subusers_o $subuser): ?string
    {
        $countryCode = $subuser->phone_country_code->value();
        $phone = $subuser->phone->value();
        if ($countryCode === null || $phone === null) {
            return null;
        }

        $normalized = preg_replace('/[^0-9]/', '', (string)$countryCode . (string)$phone);
        return $normalized !== '' ? $normalized : null;
    }

    private function destinationKey(subusers_o $subuser, string $channel): string
    {
        return $channel . ':' . (string)($this->destinationFor($subuser, $channel) ?? '');
    }

    private function key(subusers_o $subuser, string $channel): string
    {
        return 'subuser_contact_verification:' . (int)$subuser->id . ':' . $channel;
    }

    private function readChallenge(subusers_o $subuser, string $channel): ?array
    {
        if ($this->redis === null) {
            return null;
        }

        $raw = $this->redis->get($this->key($subuser, $channel));
        if (!is_string($raw) || $raw === '') {
            return null;
        }

        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    private function writeChallenge(subusers_o $subuser, string $channel, array $payload, ?int $ttl = null): void
    {
        if ($this->redis === null) {
            throw new Exception('Verification cache is unavailable');
        }

        $encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
        if ($encoded === false) {
            throw new Exception('Failed to encode verification challenge');
        }

        $this->redis->setEx($this->key($subuser, $channel), $encoded, $ttl ?? self::CODE_TTL_SECONDS);
    }

    private function deleteChallenge(subusers_o $subuser, string $channel): void
    {
        if ($this->redis !== null) {
            $this->redis->delete($this->key($subuser, $channel));
        }
    }

    private function deliverCode(subusers_o $subuser, string $channel, string $destination, string $code): void
    {
        if ($channel === self::CHANNEL_PHONE) {
            if ($this->smsSender !== null) {
                call_user_func($this->smsSender, $destination, $code, $subuser);
                return;
            }

            $gateway = new gatewayapi();
            if (!$gateway->isEnabled()) {
                throw new Exception('SMS delivery is not configured.');
            }
            $gateway->send([$destination], 'Truck Wash verifikationskode: ' . $code . '. Den udløber om 10 minutter.');
            return;
        }

        if ($this->emailSender !== null) {
            call_user_func($this->emailSender, $destination, $code, $subuser);
            return;
        }

        $recipientName = $this->normalizeNullableString($subuser->name->value()) ?? 'Chauffør';
        $message = '<p>Din verifikationskode til Truck Wash er <strong>' . htmlspecialchars($code, ENT_QUOTES, 'UTF-8') . '</strong>.</p>'
            . '<p>Koden udløber om 10 minutter.</p>';
        (new email())->sendEmail($destination, $recipientName, 'Truck Wash verifikationskode', $message);
    }

    private function markVerified(subusers_o $subuser, string $channel): void
    {
        $timestamp = date('Y-m-d H:i:s', $this->now());
        if ($channel === self::CHANNEL_EMAIL) {
            $subuser->email_verified_at->set($timestamp);
            return;
        }

        $subuser->phone_verified_at->set($timestamp);
    }

    private function markUnverified(subusers_o $subuser, string $channel): void
    {
        if ($channel === self::CHANNEL_EMAIL) {
            $subuser->email_verified_at->set(null);
            return;
        }

        $subuser->phone_verified_at->set(null);
    }

    private function verifiedAtValue(subusers_o $subuser, string $channel): ?string
    {
        $value = $channel === self::CHANNEL_EMAIL
            ? $subuser->email_verified_at->value()
            : $subuser->phone_verified_at->value();
        $value = $this->normalizeNullableString($value);
        return $value;
    }

    private function verificationState(array $emailStatus, array $phoneStatus): string
    {
        if (!$phoneStatus['available'] && !$emailStatus['available']) {
            return 'missing_contacts';
        }
        if (!$phoneStatus['available']) {
            return 'missing_phone';
        }
        if (!$emailStatus['available']) {
            return 'missing_email';
        }
        if ($phoneStatus['verified'] && $emailStatus['verified']) {
            return 'verified';
        }
        if ($phoneStatus['verified'] || $emailStatus['verified']) {
            return 'partial';
        }

        return 'unverified';
    }

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

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

    private function maskEmail(string $email): string
    {
        [$local, $domain] = array_pad(explode('@', $email, 2), 2, '');
        $prefix = substr($local, 0, 2);
        return $prefix . str_repeat('*', max(2, strlen($local) - 2)) . '@' . $domain;
    }

    private function maskPhone(string $phone): string
    {
        $suffix = substr($phone, -4);
        return str_repeat('*', max(0, strlen($phone) - 4)) . $suffix;
    }

    private function delivery(string $channel, string $status, string $message, array $extra = []): array
    {
        return [
            'channel' => $channel,
            'status' => $status,
            'message' => $message,
            ...$extra,
        ];
    }

    private function verificationResult(string $channel, string $status, string $message, array $extra = []): array
    {
        return [
            'channel' => $channel,
            'status' => $status,
            'message' => $message,
            ...$extra,
        ];
    }

    private function logEvent(string $action, subusers_o $subuser, string $channel, array $actor): void
    {
        try {
            if (!defined('redis')) {
                return;
            }
            $actorId = isset($actor['id']) ? (int)$actor['id'] : 0;
            (new logs_o())->add(
                'subusers',
                'global',
                1,
                $actorId,
                $action,
                'Chauffeur contact verification ' . $channel . ': ' . (int)$subuser->id
            );
        } catch (Exception) {
        }
    }
}
