<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use OCP\Security\ICrypto;
use Psr\Log\LoggerInterface;

/**
 * Field-level encryption service wrapping Nextcloud's ICrypto.
 *
 * Provides encrypt/decrypt operations for sensitive fields:
 * - IBAN and Kontoinhaber (banking data)
 * - Allergien (medical data)
 *
 * Encrypted values are prefixed with a marker to distinguish
 * them from plaintext during migration periods.
 *
 * Part of Issue #60.
 */
class EncryptionService {

    /** Prefix to identify encrypted values */
    private const ENCRYPTED_PREFIX = 'ENC:';

    private ICrypto $crypto;
    private LoggerInterface $logger;

    public function __construct(
        ICrypto $crypto,
        LoggerInterface $logger
    ) {
        $this->crypto = $crypto;
        $this->logger = $logger;
    }

    /**
     * Encrypt a plaintext value.
     *
     * Returns the encrypted value prefixed with the encryption marker.
     * Returns null if the input is null or empty.
     *
     * @param string|null $plaintext The value to encrypt
     * @return string|null The encrypted value with prefix, or null
     */
    public function encrypt(?string $plaintext): ?string {
        if ($plaintext === null || $plaintext === '') {
            return $plaintext;
        }

        try {
            $encrypted = $this->crypto->encrypt($plaintext);
            return self::ENCRYPTED_PREFIX . $encrypted;
        } catch (\Exception $e) {
            $this->logger->error('Encryption failed — refusing to store plaintext', [
                'exception' => $e,
                'app' => 'mitgliederverwaltung',
            ]);
            throw new \RuntimeException(
                'Verschluesselung fehlgeschlagen. Daten wurden nicht gespeichert, um unverschluesselte Ablage zu verhindern.',
                0,
                $e
            );
        }
    }

    /**
     * Decrypt an encrypted value.
     *
     * Handles graceful fallback:
     * - If the value is null/empty, returns as-is
     * - If the value is not encrypted (no prefix), returns as-is (plaintext migration support)
     * - If decryption fails (corrupted data, key rotation), returns a placeholder
     *
     * @param string|null $ciphertext The encrypted value (with prefix)
     * @return string|null The decrypted plaintext, or null
     */
    public function decrypt(?string $ciphertext): ?string {
        if ($ciphertext === null || $ciphertext === '') {
            return $ciphertext;
        }

        // If not encrypted, return as plaintext (migration support)
        if (!$this->isEncrypted($ciphertext)) {
            return $ciphertext;
        }

        try {
            $withoutPrefix = substr($ciphertext, strlen(self::ENCRYPTED_PREFIX));
            return $this->crypto->decrypt($withoutPrefix);
        } catch (\Exception $e) {
            $this->logger->warning('Decryption failed — data may be corrupted or key rotated', [
                'exception' => $e,
                'app' => 'mitgliederverwaltung',
            ]);
            return '[Entschluesselung fehlgeschlagen]';
        }
    }

    /**
     * Check if a value is encrypted (has the encryption prefix).
     *
     * Useful during migration to detect which fields have been
     * encrypted vs. which are still plaintext.
     *
     * @param string|null $value The value to check
     * @return bool True if the value has the encryption prefix
     */
    public function isEncrypted(?string $value): bool {
        if ($value === null) {
            return false;
        }
        return str_starts_with($value, self::ENCRYPTED_PREFIX);
    }

    /**
     * Decrypt a value only if the caller has appropriate permission.
     *
     * If the user is not authorized, returns a redacted placeholder
     * instead of the actual value.
     *
     * @param string|null $ciphertext The encrypted value
     * @param bool $hasPermission Whether the current user has permission to view this field
     * @return string|null The decrypted value, a redacted placeholder, or null
     */
    public function decryptIfAuthorized(?string $ciphertext, bool $hasPermission): ?string {
        if ($ciphertext === null || $ciphertext === '') {
            return $ciphertext;
        }

        if (!$hasPermission) {
            return '[Zugriff verweigert]';
        }

        return $this->decrypt($ciphertext);
    }

    /**
     * Encrypt a value only if it is not already encrypted.
     *
     * Prevents double-encryption during migration or updates.
     *
     * @param string|null $plaintext The value to encrypt
     * @return string|null The encrypted value, or the already-encrypted value
     */
    public function encryptIfNeeded(?string $plaintext): ?string {
        if ($plaintext === null || $plaintext === '') {
            return $plaintext;
        }

        // Already encrypted — don't double-encrypt
        if ($this->isEncrypted($plaintext)) {
            return $plaintext;
        }

        return $this->encrypt($plaintext);
    }
}
