<?php
declare(strict_types=1);

namespace App\Modules\Settings;

use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Support\StringHelper;
use PDO;
use Throwable;

final class SmsplanetIntegrationRepository
{
    private const INTEGRATION_TYPE = 'smsplanet';
    private const INTEGRATION_NAME = 'SMSPLANET';
    private const INTEGRATION_BASE_URL = 'https://api2.smsplanet.pl/sms';
    private const AUTH_TOKEN = 'token';
    private const AUTH_KEY_PASSWORD = 'key_password';
    private const SENDER_MODE_TEXT = 'text';
    private const SENDER_MODE_PHONE = 'phone';
    private const MAX_DEFAULT_FOOTER_LENGTH = 300;

    private readonly IntegrationsRepository $integrations;
    private readonly IntegrationSecretCipher $cipher;

    public function __construct(
        private readonly PDO $pdo,
        private readonly string $secret
    ) {
        $this->integrations = new IntegrationsRepository($this->pdo);
        $this->cipher = new IntegrationSecretCipher($this->secret);
    }

    /**
     * @return array<string, mixed>
     */
    public function getSettings(): array
    {
        $this->ensureRow();
        $integrationId = $this->ensureBaseIntegration();
        $row = $this->fetchRow() ?? [];
        $integration = $this->integrations->findById($integrationId);

        return [
            'integration_id' => $integrationId,
            'auth_method' => $this->normalizeAuthMethod((string) ($row['auth_method'] ?? '')),
            'sender' => trim((string) ($row['sender'] ?? '')),
            'sender_mode' => $this->normalizeSenderMode((string) ($row['sender_mode'] ?? '')),
            'sender_phone' => trim((string) ($row['sender_phone'] ?? '')),
            'default_footer' => $this->normalizeDefaultFooter((string) ($row['default_footer'] ?? '')),
            'clear_polish' => !empty($row['clear_polish']),
            'transactional' => !empty($row['transactional']),
            'has_api_token' => $this->hasEncryptedValue($row['api_token_encrypted'] ?? null),
            'has_api_key' => $this->hasEncryptedValue($row['api_key_encrypted'] ?? null),
            'has_api_password' => $this->hasEncryptedValue($row['api_password_encrypted'] ?? null),
            'is_active' => (int) ($integration['is_active'] ?? 1) === 1,
            'last_test_status' => trim((string) ($integration['last_test_status'] ?? '')),
            'last_test_http_code' => isset($integration['last_test_http_code']) ? (int) $integration['last_test_http_code'] : null,
            'last_test_message' => trim((string) ($integration['last_test_message'] ?? '')),
            'last_test_at' => trim((string) ($integration['last_test_at'] ?? '')),
            'updated_at' => trim((string) ($row['updated_at'] ?? '')),
        ];
    }

    /**
     * @param array<string, mixed> $payload
     */
    public function saveSettings(array $payload): void
    {
        $this->ensureRow();
        $integrationId = $this->ensureBaseIntegration();
        $row = $this->fetchRequiredRow();

        $authMethod = $this->normalizeAuthMethod((string) ($payload['auth_method'] ?? ''));
        $sender = $this->validateSender((string) ($payload['sender'] ?? ''));
        $senderMode = $this->normalizeSenderMode((string) ($payload['sender_mode'] ?? ''));
        $senderPhone = $this->validateSenderPhone((string) ($payload['sender_phone'] ?? ''), $senderMode);
        $defaultFooter = $this->validateDefaultFooter((string) ($payload['default_footer'] ?? ''));
        $tokenEncrypted = $this->resolveTokenEncrypted($row, (string) ($payload['api_token'] ?? ''));
        $keyEncrypted = $this->resolveKeyEncrypted($row, (string) ($payload['api_key'] ?? ''));
        $passwordEncrypted = $this->resolvePasswordEncrypted($row, (string) ($payload['api_password'] ?? ''));

        $this->validateCredentials($authMethod, $tokenEncrypted, $keyEncrypted, $passwordEncrypted);
        $this->updateSettingsRow(
            $authMethod,
            $tokenEncrypted,
            $keyEncrypted,
            $passwordEncrypted,
            $sender,
            $senderMode,
            $senderPhone,
            $defaultFooter,
            $payload
        );
        $this->updateIntegrationActive($integrationId, !empty($payload['is_active']));
    }

    /**
     * @return array{
     *     integration_id: int,
     *     auth_method: string,
     *     api_token: string,
     *     api_key: string,
     *     api_password: string,
     *     sender: string,
     *     sender_mode: string,
     *     sender_phone: string,
     *     default_footer: string,
     *     clear_polish: bool,
     *     transactional: bool
     * }|null
     */
    public function getCredentials(): ?array
    {
        $this->ensureRow();
        $integrationId = $this->ensureBaseIntegration();
        $row = $this->fetchRow();
        $integration = $this->integrations->findById($integrationId);
        if ($row === null || (int) ($integration['is_active'] ?? 0) !== 1) {
            return null;
        }

        $authMethod = $this->normalizeAuthMethod((string) ($row['auth_method'] ?? ''));
        $sender = trim((string) ($row['sender'] ?? ''));
        $senderMode = $this->normalizeSenderMode((string) ($row['sender_mode'] ?? ''));
        $senderPhone = trim((string) ($row['sender_phone'] ?? ''));
        $apiSender = $senderMode === self::SENDER_MODE_PHONE ? $senderPhone : $sender;
        $apiToken = $this->decryptValue((string) ($row['api_token_encrypted'] ?? ''));
        $apiKey = $this->decryptValue((string) ($row['api_key_encrypted'] ?? ''));
        $apiPassword = $this->decryptValue((string) ($row['api_password_encrypted'] ?? ''));

        if (!$this->hasCompleteCredentials($authMethod, $apiSender, $apiToken, $apiKey, $apiPassword)) {
            return null;
        }

        return [
            'integration_id' => $integrationId,
            'auth_method' => $authMethod,
            'api_token' => $apiToken,
            'api_key' => $apiKey,
            'api_password' => $apiPassword,
            'sender' => $apiSender,
            'sender_mode' => $senderMode,
            'sender_phone' => $senderPhone,
            'default_footer' => $this->normalizeDefaultFooter((string) ($row['default_footer'] ?? '')),
            'clear_polish' => !empty($row['clear_polish']),
            'transactional' => !empty($row['transactional']),
        ];
    }

    private function ensureBaseIntegration(): int
    {
        return $this->integrations->ensureIntegration(
            self::INTEGRATION_TYPE,
            self::INTEGRATION_NAME,
            self::INTEGRATION_BASE_URL,
            15,
            true
        );
    }

    private function ensureRow(): void
    {
        $integrationId = $this->ensureBaseIntegration();
        $statement = $this->pdo->prepare(
            'INSERT INTO smsplanet_integration_settings (id, integration_id, created_at, updated_at)
             VALUES (1, :integration_id, NOW(), NOW())
             ON DUPLICATE KEY UPDATE integration_id = VALUES(integration_id), updated_at = VALUES(updated_at)'
        );
        $statement->execute(['integration_id' => $integrationId]);
    }

    /**
     * @return array<string, mixed>|null
     */
    private function fetchRow(): ?array
    {
        try {
            $statement = $this->pdo->prepare('SELECT * FROM smsplanet_integration_settings WHERE id = 1 LIMIT 1');
            $statement->execute();
            $row = $statement->fetch(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return null;
        }

        return is_array($row) ? $row : null;
    }

    /**
     * @return array<string, mixed>
     */
    private function fetchRequiredRow(): array
    {
        $row = $this->fetchRow();
        if ($row === null) {
            throw new IntegrationConfigException('Brak rekordu konfiguracji SMSPLANET.');
        }

        return $row;
    }

    private function normalizeAuthMethod(string $value): string
    {
        return $value === self::AUTH_KEY_PASSWORD ? self::AUTH_KEY_PASSWORD : self::AUTH_TOKEN;
    }

    private function normalizeSenderMode(string $value): string
    {
        return $value === self::SENDER_MODE_PHONE ? self::SENDER_MODE_PHONE : self::SENDER_MODE_TEXT;
    }

    private function validateSender(string $value): string
    {
        $sender = trim($value);
        if ($sender === '' || strlen($sender) > 32) {
            throw new IntegrationConfigException('Podaj pole nadawcy SMSPLANET (maks. 32 znaki).');
        }

        return $sender;
    }

    private function validateSenderPhone(string $value, string $senderMode): ?string
    {
        $phone = preg_replace('/[\s+\-()]/', '', trim($value)) ?? '';
        if ($senderMode !== self::SENDER_MODE_PHONE) {
            return $phone !== '' ? substr($phone, 0, 32) : null;
        }

        if (preg_match('/^\d{8,15}$/', $phone) !== 1) {
            throw new IntegrationConfigException('Podaj numer 2WAY w formacie 600111222 albo 48600111222.');
        }

        return $phone;
    }

    private function normalizeDefaultFooter(string $value): string
    {
        return trim(str_replace(["\r\n", "\r"], "\n", $value));
    }

    private function validateDefaultFooter(string $value): ?string
    {
        $footer = $this->normalizeDefaultFooter($value);
        if ($footer === '') {
            return null;
        }
        if ($this->messageLength($footer) > self::MAX_DEFAULT_FOOTER_LENGTH) {
            throw new IntegrationConfigException('Stopka SMSPLANET nie moze przekraczac 300 znakow.');
        }

        return $footer;
    }

    /**
     * @param array<string, mixed> $row
     */
    private function resolveTokenEncrypted(array $row, string $newToken): ?string
    {
        $token = trim($newToken);
        if ($token !== '') {
            return $this->cipher->encrypt($token);
        }

        return StringHelper::nullableString((string) ($row['api_token_encrypted'] ?? ''));
    }

    /**
     * @param array<string, mixed> $row
     */
    private function resolveKeyEncrypted(array $row, string $newKey): ?string
    {
        $key = trim($newKey);
        if ($key !== '') {
            return $this->cipher->encrypt($key);
        }

        return StringHelper::nullableString((string) ($row['api_key_encrypted'] ?? ''));
    }

    /**
     * @param array<string, mixed> $row
     */
    private function resolvePasswordEncrypted(array $row, string $newPassword): ?string
    {
        $password = trim($newPassword);
        if ($password !== '') {
            return $this->cipher->encrypt($password);
        }

        return StringHelper::nullableString((string) ($row['api_password_encrypted'] ?? ''));
    }

    private function validateCredentials(
        string $authMethod,
        ?string $tokenEncrypted,
        ?string $keyEncrypted,
        ?string $passwordEncrypted
    ): void {
        if ($authMethod === self::AUTH_TOKEN && ($tokenEncrypted === null || $tokenEncrypted === '')) {
            throw new IntegrationConfigException('Podaj token Bearer SMSPLANET.');
        }

        if ($authMethod !== self::AUTH_KEY_PASSWORD) {
            return;
        }

        if ($keyEncrypted === null || $keyEncrypted === '') {
            throw new IntegrationConfigException('Podaj klucz API SMSPLANET.');
        }
        if ($passwordEncrypted === null || $passwordEncrypted === '') {
            throw new IntegrationConfigException('Podaj haslo API SMSPLANET.');
        }
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function updateSettingsRow(
        string $authMethod,
        ?string $tokenEncrypted,
        ?string $keyEncrypted,
        ?string $passwordEncrypted,
        string $sender,
        string $senderMode,
        ?string $senderPhone,
        ?string $defaultFooter,
        array $payload
    ): void {
        $statement = $this->pdo->prepare(
            'UPDATE smsplanet_integration_settings
             SET auth_method = :auth_method,
                 api_token_encrypted = :api_token_encrypted,
                 api_key_encrypted = :api_key_encrypted,
                 api_password_encrypted = :api_password_encrypted,
                 sender = :sender,
                 sender_mode = :sender_mode,
                 sender_phone = :sender_phone,
                 default_footer = :default_footer,
                 clear_polish = :clear_polish,
                 transactional = :transactional,
                 updated_at = NOW()
             WHERE id = 1'
        );
        $statement->execute([
            'auth_method' => $authMethod,
            'api_token_encrypted' => $tokenEncrypted,
            'api_key_encrypted' => $keyEncrypted,
            'api_password_encrypted' => $passwordEncrypted,
            'sender' => $sender,
            'sender_mode' => $senderMode,
            'sender_phone' => $senderPhone,
            'default_footer' => $defaultFooter,
            'clear_polish' => !empty($payload['clear_polish']) ? 1 : 0,
            'transactional' => !empty($payload['transactional']) ? 1 : 0,
        ]);
    }

    private function updateIntegrationActive(int $integrationId, bool $isActive): void
    {
        $statement = $this->pdo->prepare(
            'UPDATE integrations
             SET is_active = :is_active,
                 updated_at = NOW()
             WHERE id = :id AND type = :type'
        );
        $statement->execute([
            'id' => $integrationId,
            'type' => self::INTEGRATION_TYPE,
            'is_active' => $isActive ? 1 : 0,
        ]);
    }

    private function hasEncryptedValue(mixed $value): bool
    {
        return trim((string) $value) !== '';
    }

    private function decryptValue(string $encrypted): string
    {
        $value = StringHelper::nullableString($encrypted);
        if ($value === null) {
            return '';
        }

        return trim((string) $this->cipher->decrypt($value));
    }

    private function hasCompleteCredentials(
        string $authMethod,
        string $sender,
        string $apiToken,
        string $apiKey,
        string $apiPassword
    ): bool {
        if ($sender === '') {
            return false;
        }

        if ($authMethod === self::AUTH_TOKEN) {
            return $apiToken !== '';
        }

        return $apiKey !== '' && $apiPassword !== '';
    }

    private function messageLength(string $value): int
    {
        if (function_exists('mb_strlen')) {
            return mb_strlen($value, 'UTF-8');
        }

        return strlen($value);
    }
}
