<?php

namespace App\Service;

use App\Exception\VaultException;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
 * @codeCoverageIgnore Wrapper Hashicorp Vault Transit API (service externe)
 */
class VaultService
{
    private const TRANSIT_KEYS_PATH = '/v1/transit/keys/';

    public function __construct(
        private HttpClientInterface $httpClient,
        private LoggerInterface $logger,
        #[Autowire(env: 'VAULT_URL')] private string $vaultUrl = '',
        #[Autowire(env: 'VAULT_TOKEN')] private string $vaultToken = '',
    ) {
    }

    public function isAvailable(): bool
    {
        return '' !== $this->vaultUrl && '' !== $this->vaultToken;
    }

    // ──── Gestion des clés Transit ─────────────────────

    /**
     * Crée une clé de chiffrement Transit.
     */
    public function createKey(string $keyName, string $type = 'aes256-gcm96'): bool
    {
        if (!$this->isAvailable()) {
            return false;
        }

        try {
            $this->request('POST', self::TRANSIT_KEYS_PATH.$keyName, ['type' => $type]);

            $this->logger->info('Vault: cle creee: '.$keyName);

            return true;
        } catch (\Throwable $e) {
            $this->logger->error('Vault: erreur creation cle '.$keyName.': '.$e->getMessage());

            return false;
        }
    }

    /**
     * Supprime une clé Transit (doit être marquée deletable d'abord).
     */
    public function deleteKey(string $keyName): bool
    {
        if (!$this->isAvailable()) {
            return false;
        }

        try {
            // Marquer comme supprimable
            $this->request('POST', self::TRANSIT_KEYS_PATH.$keyName.'/config', ['deletion_allowed' => true]);
            // Supprimer
            $this->request('DELETE', self::TRANSIT_KEYS_PATH.$keyName);

            $this->logger->info('Vault: cle supprimee: '.$keyName);

            return true;
        } catch (\Throwable $e) {
            $this->logger->error('Vault: erreur suppression cle '.$keyName.': '.$e->getMessage());

            return false;
        }
    }

    /**
     * Met à jour la configuration d'une clé (rotation, export, etc.).
     *
     * @param array<string, mixed> $config
     */
    public function updateKey(string $keyName, array $config): bool
    {
        if (!$this->isAvailable()) {
            return false;
        }

        try {
            $this->request('POST', self::TRANSIT_KEYS_PATH.$keyName.'/config', $config);

            return true;
        } catch (\Throwable $e) {
            $this->logger->error('Vault: erreur update cle '.$keyName.': '.$e->getMessage());

            return false;
        }
    }

    /**
     * Liste toutes les clés Transit.
     *
     * @return list<string>
     */
    public function listKeys(): array
    {
        if (!$this->isAvailable()) {
            return [];
        }

        try {
            $data = $this->request('LIST', self::TRANSIT_KEYS_PATH);

            return $data['data']['keys'] ?? [];
        } catch (\Throwable) {
            return [];
        }
    }

    /**
     * Vérifie si une clé existe.
     */
    public function keyExists(string $keyName): bool
    {
        if (!$this->isAvailable()) {
            return false;
        }

        try {
            $this->request('GET', self::TRANSIT_KEYS_PATH.$keyName);

            return true;
        } catch (\Throwable) {
            return false;
        }
    }

    /**
     * Vérifie si la clé existe, sinon la crée.
     */
    public function checkOrCreateKey(string $keyName, string $type = 'aes256-gcm96'): bool
    {
        if ($this->keyExists($keyName)) {
            return true;
        }

        return $this->createKey($keyName, $type);
    }

    // ──── Chiffrement / Déchiffrement ──────────────────

    /**
     * Chiffre des données avec une clé Transit.
     * Retourne le ciphertext (vault:v1:...).
     */
    public function encrypt(string $keyName, string $plaintext): ?string
    {
        if (!$this->isAvailable()) {
            return null;
        }

        $this->checkOrCreateKey($keyName);

        try {
            $data = $this->request('POST', '/v1/transit/encrypt/'.$keyName, [
                'plaintext' => base64_encode($plaintext),
            ]);

            return $data['data']['ciphertext'] ?? null;
        } catch (\Throwable $e) {
            $this->logger->error('Vault: erreur encrypt ('.$keyName.'): '.$e->getMessage());

            return null;
        }
    }

    /**
     * Déchiffre des données avec une clé Transit.
     */
    public function decrypt(string $keyName, string $ciphertext): ?string
    {
        if (!$this->isAvailable()) {
            return null;
        }

        try {
            $data = $this->request('POST', '/v1/transit/decrypt/'.$keyName, [
                'ciphertext' => $ciphertext,
            ]);

            $b64 = $data['data']['plaintext'] ?? null;

            return null !== $b64 ? base64_decode($b64) : null;
        } catch (\Throwable $e) {
            $this->logger->error('Vault: erreur decrypt ('.$keyName.'): '.$e->getMessage());

            return null;
        }
    }

    // ──── HTTP ─────────────────────────────────────────

    /**
     * @param array<string, mixed>|null $body
     *
     * @return array<string, mixed>
     */
    private function request(string $method, string $path, ?array $body = null): array
    {
        $options = [
            'headers' => [
                'X-Vault-Token' => $this->vaultToken,
                'Content-Type' => 'application/json',
            ],
        ];

        if (null !== $body) {
            $options['json'] = $body;
        }

        $response = $this->httpClient->request($method, rtrim($this->vaultUrl, '/').$path, $options);

        if ($response->getStatusCode() >= 400) {
            throw VaultException::httpError($response->getStatusCode(), $response->getContent(false));
        }

        $content = $response->getContent(false);

        return '' !== $content ? json_decode($content, true) ?? [] : [];
    }
}
