<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use OCP\IUserSession;
use Psr\Log\LoggerInterface;

/**
 * Wraps export content (CSV/PDF) in a password-protected AES-256 ZIP archive.
 *
 * Uses PHP's ZipArchive with EM_AES_256 encryption. If AES encryption
 * is not available (older libzip), falls back to EM_TRAD_PKWARE and
 * logs a warning.
 *
 * Part of Issue #48.
 */
class EncryptedExportService {

    private AuditService $auditService;
    private PermissionService $permissionService;
    private IUserSession $userSession;
    private LoggerInterface $logger;

    public function __construct(
        AuditService $auditService,
        PermissionService $permissionService,
        IUserSession $userSession,
        LoggerInterface $logger
    ) {
        $this->auditService = $auditService;
        $this->permissionService = $permissionService;
        $this->userSession = $userSession;
        $this->logger = $logger;
    }

    /**
     * Create an encrypted export archive.
     *
     * Wraps the given content in a password-protected ZIP and logs
     * the export action to the audit log.
     *
     * @param string $content The raw export content (CSV string, PDF binary, etc.)
     * @param string $innerFilename The filename for the file inside the ZIP
     * @param string $password The user-provided password for encryption
     * @param string $exportType Human-readable export type for audit logging (e.g. 'Mitgliederliste')
     * @return array{content: string, filename: string}
     * @throws \RuntimeException If ZIP creation fails
     */
    public function createEncryptedExport(
        string $content,
        string $innerFilename,
        string $password,
        string $exportType
    ): array {
        $this->validatePassword($password);

        $zipContent = $this->createEncryptedZip($content, $innerFilename, $password);

        // Audit log the export
        $userId = $this->getCurrentUserId();
        $this->auditService->logCreate(
            [
                'export_type' => $exportType,
                'filename' => $innerFilename,
                'encrypted' => 'true',
                'user' => $userId,
            ],
            'encrypted_export',
            0
        );

        $date = (new \DateTime())->format('Y-m-d_His');
        $zipFilename = "Export_{$exportType}_{$date}.zip";

        return [
            'content' => $zipContent,
            'filename' => $zipFilename,
        ];
    }

    /**
     * Check if the current user has permission to export banking data.
     *
     * @throws ValidationException If the user lacks banking visibility
     */
    public function assertBankingExportPermission(): void {
        $userId = $this->getCurrentUserId();

        if (!$this->permissionService->canSeeBanking($userId)) {
            throw new ValidationException(
                'Keine Berechtigung: Bankdaten-Export erfordert Bankdaten-Sichtbarkeit.'
            );
        }
    }

    /**
     * Create a password-protected ZIP archive containing the given content.
     *
     * @param string $content File content to include
     * @param string $filename Name of the file inside the ZIP
     * @param string $password Encryption password
     * @return string The ZIP file content as a binary string
     * @throws \RuntimeException If ZIP creation fails
     */
    private function createEncryptedZip(string $content, string $filename, string $password): string {
        if (!class_exists(\ZipArchive::class)) {
            throw new \RuntimeException(
                'ZipArchive ist nicht verfuegbar. Bitte php-zip installieren.'
            );
        }

        $tmpFile = tempnam(sys_get_temp_dir(), 'mv_export_');
        if ($tmpFile === false) {
            throw new \RuntimeException('Temporaere Datei konnte nicht erstellt werden.');
        }

        // Restrict permissions to owner-only to prevent other processes
        // from reading sensitive export data during the temp file's lifetime
        chmod($tmpFile, 0600);

        // Ensure we clean up the temp file even on failure
        try {
            $zip = new \ZipArchive();
            $result = $zip->open($tmpFile, \ZipArchive::OVERWRITE);

            if ($result !== true) {
                throw new \RuntimeException(
                    'ZIP-Archiv konnte nicht erstellt werden. Fehlercode: ' . $result
                );
            }

            // Set password for the archive
            if (!$zip->setPassword($password)) {
                $zip->close();
                throw new \RuntimeException('ZIP-Passwort konnte nicht gesetzt werden.');
            }

            // Add the file content
            if (!$zip->addFromString($filename, $content)) {
                $zip->close();
                throw new \RuntimeException('Datei konnte nicht zum ZIP hinzugefuegt werden.');
            }

            // Set encryption on the file
            $encryptionMethod = $this->getEncryptionMethod();
            if (!$zip->setEncryptionName($filename, $encryptionMethod)) {
                $this->logger->warning('ZIP-Verschluesselung konnte nicht gesetzt werden, Fallback auf Passwortschutz', [
                    'method' => $encryptionMethod,
                    'app' => 'mitgliederverwaltung',
                ]);
            }

            $zip->close();

            $zipContent = file_get_contents($tmpFile);
            if ($zipContent === false) {
                throw new \RuntimeException('ZIP-Datei konnte nicht gelesen werden.');
            }

            return $zipContent;
        } finally {
            // Always clean up temp file
            if (file_exists($tmpFile)) {
                unlink($tmpFile);
            }
        }
    }

    /**
     * Determine the best available encryption method.
     *
     * Prefers AES-256, falls back to traditional PKWARE if AES is
     * not supported by the installed libzip version.
     *
     * @return int ZipArchive encryption constant
     */
    private function getEncryptionMethod(): int {
        // AES-256 is available in PHP 7.2+ with libzip >= 1.2.0
        if (defined('ZipArchive::EM_AES_256')) {
            return \ZipArchive::EM_AES_256;
        }

        $this->logger->warning(
            'AES-256 Verschluesselung nicht verfuegbar, verwende PKWARE-Verschluesselung als Fallback',
            ['app' => 'mitgliederverwaltung']
        );

        if (defined('ZipArchive::EM_TRAD_PKWARE')) {
            return \ZipArchive::EM_TRAD_PKWARE;
        }

        // Last resort: no encryption constant available
        return 0;
    }

    /**
     * Validate that the password meets minimum security requirements.
     *
     * @throws ValidationException If the password is too short or empty
     */
    private function validatePassword(string $password): void {
        if (strlen($password) < 8) {
            throw new ValidationException(
                'Passwort muss mindestens 8 Zeichen lang sein.'
            );
        }
    }

    private function getCurrentUserId(): string {
        $user = $this->userSession->getUser();
        return $user ? $user->getUID() : 'system';
    }
}
