<?php
// web-files/libs/CpanelImportSummary.php
//
// Reads, writes, and redacts the per-import JSON summary file that the
// results page renders and the cron sweeper cleans up.
//
// Storage: /var/lib/whp/cpanel-imports/<import_id>.json (root-owned, 0600)
// Temp passwords inside the file are AES-256-GCM-encrypted with a per-import
// key derived from NONCE_SALT via HKDF-SHA256.

class CpanelImportSummary {
    public const STORAGE_DIR = '/var/lib/whp/cpanel-imports';
    private const HKDF_INFO_PREFIX = 'cpanel-import-temp-password:';

    /**
     * Encrypt a temp password for storage in the summary JSON.
     * Returns base64(iv || ciphertext || tag), 12-byte IV + 16-byte tag.
     */
    public static function encryptTempPassword(string $importId, string $plaintext): string {
        if (!defined('NONCE_SALT') || NONCE_SALT === '') {
            throw new RuntimeException('NONCE_SALT not defined');
        }
        $key = hash_hkdf('sha256', NONCE_SALT, 32, self::HKDF_INFO_PREFIX . $importId);
        $iv  = random_bytes(12);
        $tag = '';
        $ct  = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
        if ($ct === false) {
            throw new RuntimeException('openssl_encrypt failed');
        }
        return base64_encode($iv . $ct . $tag);
    }

    /**
     * Decrypt a previously-encrypted temp password.
     */
    public static function decryptTempPassword(string $importId, string $encoded): string {
        if (!defined('NONCE_SALT') || NONCE_SALT === '') {
            throw new RuntimeException('NONCE_SALT not defined');
        }
        $key  = hash_hkdf('sha256', NONCE_SALT, 32, self::HKDF_INFO_PREFIX . $importId);
        $blob = base64_decode($encoded, true);
        if ($blob === false || strlen($blob) < 12 + 16 + 1) {
            throw new RuntimeException('temp password ciphertext is malformed');
        }
        $iv  = substr($blob, 0, 12);
        $tag = substr($blob, -16);
        $ct  = substr($blob, 12, -16);
        $pt  = openssl_decrypt($ct, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
        if ($pt === false) {
            throw new RuntimeException('openssl_decrypt failed (auth tag mismatch?)');
        }
        return $pt;
    }

    public static function summaryPath(string $importId): string {
        return self::STORAGE_DIR . '/' . basename($importId) . '.json';
    }

    public static function writeSummary(string $importId, array $summary): void {
        $path = self::summaryPath($importId);
        if (!is_dir(self::STORAGE_DIR)) {
            @mkdir(self::STORAGE_DIR, 0700, true);
        }
        $tmp = $path . '.tmp.' . bin2hex(random_bytes(4));
        $json = json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            throw new RuntimeException('json_encode of summary failed');
        }
        if (file_put_contents($tmp, $json, LOCK_EX) === false) {
            throw new RuntimeException("could not write $tmp");
        }
        @chmod($tmp, 0600);
        if (!rename($tmp, $path)) {
            @unlink($tmp);
            throw new RuntimeException("could not atomically install $path");
        }
    }

    public static function readSummary(string $importId): ?array {
        $path = self::summaryPath($importId);
        if (!is_file($path)) return null;
        $raw = file_get_contents($path);
        if ($raw === false) return null;
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    /**
     * Zero every mailbox AND mysql_user temp_password_encrypted in-place,
     * stamp who/when. Both credential classes share the same lifecycle —
     * 24h auto-expire, operator "I have stored these" button — so they
     * clear together.
     */
    public static function clearCredentials(string $importId, string $actor): bool {
        $summary = self::readSummary($importId);
        if ($summary === null) return false;
        if (isset($summary['mailboxes']) && is_array($summary['mailboxes'])) {
            foreach ($summary['mailboxes'] as &$mb) {
                $mb['temp_password_encrypted'] = null;
            }
            unset($mb);
        }
        if (isset($summary['mysql_users']) && is_array($summary['mysql_users'])) {
            foreach ($summary['mysql_users'] as &$mu) {
                $mu['temp_password_encrypted'] = null;
            }
            unset($mu);
        }
        $summary['credentials_cleared_at'] = gmdate('c');
        $summary['credentials_cleared_by'] = $actor;
        self::writeSummary($importId, $summary);
        return true;
    }

    /** Persist the operator's choice on the MX panel. $action ∈ {'repointed', 'dismissed'}. */
    public static function markMxAction(string $importId, string $action, string $actor): bool {
        $summary = self::readSummary($importId);
        if ($summary === null) return false;
        if (!in_array($action, ['repointed', 'dismissed'], true)) {
            throw new InvalidArgumentException("invalid mx action: $action");
        }
        if (!isset($summary['dns']) || !is_array($summary['dns'])) {
            $summary['dns'] = [];
        }
        $summary['dns']['mx_repoint_action'] = $action;
        $summary['dns']['mx_repoint_actor']  = $actor;
        $summary['dns']['mx_repoint_at']     = gmdate('c');
        self::writeSummary($importId, $summary);
        return true;
    }
}
