<?php
defined('ABSPATH') or die('Direct access not allowed');

/**
 * VollstartRD_FileUpload — File upload handling.
 *
 * SRP: Handles upload, validation, storage, serving, and cleanup of
 * files attached to repairs and products. Stores files in own directory
 * (not WP Media Library) with .htaccess protection and download tokens.
 *
 * Pattern from Appointment Desk class-file-upload.php.
 */
class VollstartRD_FileUpload {

    private $MAIN;

    private array $dangerousExtensions = ['.php', '.phtml', '.phar', '.php3', '.php4', '.php5'];
    private array $dangerousContent = ['<?php', '<?=', '<script', '<%'];
    private array $allowedImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    private array $allowedDocTypes = ['application/pdf'];

    public function __construct($MAIN) {
        $this->MAIN = $MAIN;
    }

    /**
     * Pre-sanitize a $_FILES['xxx'] entry before it crosses any other code.
     *
     * Returns a clean array with safe scalar values, or null if the upload
     * is malformed. The 'type' field reported by the browser is intentionally
     * dropped — we always re-derive the mime type from the filename via
     * wp_check_filetype() so attacker-controlled metadata is never trusted.
     */
    public static function sanitizeUploadedFile($fileData): ?array {
        if (!is_array($fileData)) return null;
        if (empty($fileData['tmp_name']) || !is_string($fileData['tmp_name'])) return null;
        if (!is_uploaded_file($fileData['tmp_name'])) return null;
        $name = isset($fileData['name']) && is_string($fileData['name']) ? $fileData['name'] : '';
        $error = isset($fileData['error']) ? (int) $fileData['error'] : UPLOAD_ERR_NO_FILE;
        $size  = isset($fileData['size']) ? (int) $fileData['size'] : 0;
        return [
            'name'     => sanitize_file_name($name),
            'tmp_name' => $fileData['tmp_name'],
            'error'    => $error,
            'size'     => $size,
            // 'type' is intentionally omitted — handleUpload() derives mime
            // from the filename via wp_check_filetype() and never trusts the
            // browser-supplied value.
        ];
    }

    // --- Upload ---

    /**
     * Handle a file upload.
     *
     * @param array  $fileData    $_FILES['xxx'] array
     * @param string $entityType  'repair', 'product', 'staff', or 'temp'
     * @param int    $entityId    Entity ID (0 for temp)
     * @param string $caption     Optional caption/label
     * @param string $phase       Optional phase (e.g. 'intake', 'progress', 'done')
     * @param string $tempToken   Optional temp session token (for pre-create uploads)
     * @return int|WP_Error File ID on success
     */
    public function handleUpload(array $fileData, string $entityType, int $entityId, string $caption = '', string $phase = '', string $tempToken = '') {
        // Validate
        $valid = $this->validateFile($fileData);
        if (is_wp_error($valid)) {
            return $valid;
        }

        $storageHash = bin2hex(random_bytes(16));
        $downloadToken = $this->generateUniqueToken();
        $originalName = $fileData['name'];
        $storedName = sanitize_file_name($originalName);

        $storageDir = $this->getStorageDir() . '/' . $storageHash;
        if (!wp_mkdir_p($storageDir)) {
            return new \WP_Error('dir_create_failed', __('Could not create storage directory.', 'vollstart-repair-desk'));
        }

        $this->ensureHtaccess();

        $destination = $storageDir . '/' . $storedName;

        // Use wp_handle_upload() overrides for custom directory, then move
        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();

        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- is_uploaded_file check is done in validateFile()
        $tmpContent = $wp_filesystem->get_contents($fileData['tmp_name']);
        if ($tmpContent === false || !$wp_filesystem->put_contents($destination, $tmpContent, FS_CHMOD_FILE)) {
            return new \WP_Error('move_failed', __('Could not move uploaded file.', 'vollstart-repair-desk'));
        }

        $filetype = wp_check_filetype($storedName);
        if (empty($filetype['type'])) {
            return new \WP_Error('bad_mime', __('File type is not supported.', 'vollstart-repair-desk'));
        }

        // Determine which table column to use
        $fields = [
            'original_name'  => $originalName,
            'stored_name'    => $storedName,
            'storage_hash'   => $storageHash,
            'mime_type'       => $filetype['type'],
            'file_size'       => (int) $fileData['size'],
            'download_token'  => $downloadToken,
            'caption'         => sanitize_text_field($caption),
            'phase'           => sanitize_text_field($phase),
            'created_at'      => current_time('mysql'),
        ];

        $fields['entity_type'] = $entityType;
        $fields['entity_id'] = $entityId;
        if (!empty($tempToken)) {
            $fields['temp_token'] = $tempToken;
        }

        return $this->MAIN->getDB()->insert('files', $fields);
    }

    /**
     * Handle a base64 image upload (from webcam/camera capture).
     *
     * @param string $base64Data  Base64 encoded image (with or without data URI prefix)
     * @param string $entityType  'repair', 'product', 'staff', or 'temp'
     * @param int    $entityId    Entity ID (0 for temp)
     * @param string $caption     Optional caption
     * @param string $phase       Optional phase
     * @param string $tempToken   Optional temp session token
     * @return int|WP_Error File ID on success
     */
    public function handleBase64Upload(string $base64Data, string $entityType, int $entityId, string $caption = '', string $phase = '', string $tempToken = '') {
        // Strip data URI prefix if present
        if (strpos($base64Data, 'data:') === 0) {
            $parts = explode(',', $base64Data, 2);
            $base64Data = $parts[1] ?? '';
            // Extract mime type
            preg_match('/data:([^;]+)/', $parts[0], $matches);
            $mimeType = $matches[1] ?? 'image/jpeg';
        } else {
            $mimeType = 'image/jpeg';
        }

        unset($mimeType); // Client-declared mime from data: URI is never trusted.

        $decoded = base64_decode($base64Data);
        if ($decoded === false || strlen($decoded) < 100) {
            return new \WP_Error('invalid_data', __('Invalid image data.', 'vollstart-repair-desk'));
        }

        // Enforce the same size limit as validateFile() (10 MB).
        if (strlen($decoded) > 10 * 1024 * 1024) {
            return new \WP_Error('too_large', __('File too large. Maximum 10 MB.', 'vollstart-repair-desk'));
        }

        // Re-derive the REAL mime type from the decoded bytes — never trust the
        // client-declared data: URI mime. getimagesizefromstring() and
        // finfo are content-based.
        $mimeType = '';
        $imgInfo = @getimagesizefromstring($decoded);
        if ($imgInfo !== false && !empty($imgInfo['mime'])) {
            $mimeType = $imgInfo['mime'];
        } elseif (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            if ($finfo !== false) {
                $detected = finfo_buffer($finfo, $decoded);
                finfo_close($finfo);
                if (is_string($detected) && $detected !== '') {
                    $mimeType = $detected;
                }
            }
        }

        // Reject anything not in the same image whitelist the normal path allows.
        if (!in_array($mimeType, $this->allowedImageTypes, true)) {
            return new \WP_Error('bad_mime', __('File type is not supported.', 'vollstart-repair-desk'));
        }

        $ext = 'jpg';
        if ($mimeType === 'image/png') $ext = 'png';
        elseif ($mimeType === 'image/webp') $ext = 'webp';
        elseif ($mimeType === 'image/gif') $ext = 'gif';

        $storageHash = bin2hex(random_bytes(16));
        $downloadToken = $this->generateUniqueToken();
        $originalName = 'capture_' . wp_date('Y-m-d_H-i-s') . '.' . $ext;
        $storedName = $originalName;

        $storageDir = $this->getStorageDir() . '/' . $storageHash;
        if (!wp_mkdir_p($storageDir)) {
            return new \WP_Error('dir_create_failed', __('Could not create storage directory.', 'vollstart-repair-desk'));
        }

        $this->ensureHtaccess();

        $destination = $storageDir . '/' . $storedName;

        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();

        $written = $wp_filesystem->put_contents($destination, $decoded, FS_CHMOD_FILE);
        if (!$written) {
            return new \WP_Error('write_failed', __('Could not write file.', 'vollstart-repair-desk'));
        }

        $fields = [
            'original_name'  => $originalName,
            'stored_name'    => $storedName,
            'storage_hash'   => $storageHash,
            'mime_type'       => $mimeType,
            'file_size'       => strlen($decoded),
            'download_token'  => $downloadToken,
            'caption'         => sanitize_text_field($caption),
            'phase'           => sanitize_text_field($phase),
            'created_at'      => current_time('mysql'),
        ];

        $fields['entity_type'] = $entityType;
        $fields['entity_id'] = $entityId;
        if (!empty($tempToken)) {
            $fields['temp_token'] = $tempToken;
        }

        return $this->MAIN->getDB()->insert('files', $fields);
    }

    // --- Read ---

    /**
     * Get files for an entity (repair, product, staff, ...).
     */
    public function getByEntity(string $entityType, int $entityId): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        return $db->getResults("SELECT * FROM %i WHERE entity_type = %s AND entity_id = %d ORDER BY created_at ASC", $tbl, $entityType, $entityId);
    }

    /**
     * Get a file by ID.
     */
    public function getById(int $id): ?array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        return $db->getRow("SELECT * FROM %i WHERE id = %d", $tbl, $id);
    }

    /**
     * Get a file by download token.
     */
    public function getByToken(string $token): ?array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        return $db->getRow("SELECT * FROM %i WHERE download_token = %s", $tbl, $token);
    }

    /**
     * Get files by temp token (for create form gallery).
     */
    public function getByTempToken(string $tempToken): array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        return $db->getResults("SELECT * FROM %i WHERE temp_token = %s ORDER BY created_at ASC", $tbl, $tempToken);
    }

    /**
     * Count files for an entity.
     */
    public function countByEntity(string $entityType, int $entityId): int {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        return (int) $db->getVar("SELECT COUNT(*) FROM %i WHERE entity_type = %s AND entity_id = %d", $tbl, $entityType, $entityId);
    }

    // --- Delete ---

    /**
     * Delete a file (DB record + physical file).
     */
    public function delete(int $id): bool {
        $file = $this->getById($id);
        if (!$file) {
            return false;
        }

        // Delete physical file
        $path = $this->getFilePath($file);
        if ($path && file_exists($path)) {
            wp_delete_file($path);
        }

        // Delete directory if empty
        $dir = $this->getStorageDir() . '/' . $file['storage_hash'];
        if (is_dir($dir) && $this->isDirectoryEmpty($dir)) {
            global $wp_filesystem;
            if (!function_exists('WP_Filesystem')) {
                require_once ABSPATH . 'wp-admin/includes/file.php';
            }
            WP_Filesystem();
            $wp_filesystem->rmdir($dir);
        }

        return $this->MAIN->getDB()->delete('files', ['id' => $id]) !== false;
    }

    /**
     * Delete all files for an entity.
     */
    public function deleteByEntity(string $entityType, int $entityId): void {
        $files = $this->getByEntity($entityType, $entityId);
        foreach ($files as $file) {
            $this->delete($file['id']);
        }
    }

    // --- Serve ---

    /**
     * Serve a file for download/display via token.
     */
    public function serveFile(string $token): void {
        $file = $this->getByToken($token);
        if (!$file) {
            wp_die(esc_html(__('File not found.', 'vollstart-repair-desk')), 404);
        }

        $path = $this->getFilePath($file);
        if (!$path || !file_exists($path)) {
            wp_die(esc_html(__('File not found on disk.', 'vollstart-repair-desk')), 404);
        }

        $mime = $file['mime_type'] ?: 'application/octet-stream';
        $isImage = strpos($mime, 'image/') === 0;

        header('Content-Type: ' . $mime);
        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();
        header('Content-Length: ' . $wp_filesystem->size($path));
        $isPdf = $mime === 'application/pdf';
        if ($isImage || $isPdf) {
            header('Content-Disposition: inline; filename="' . sanitize_file_name($file['original_name']) . '"');
        } else {
            header('Content-Disposition: attachment; filename="' . sanitize_file_name($file['original_name']) . '"');
        }
        header('Cache-Control: private, max-age=86400');

        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- binary file data, not HTML
        echo $wp_filesystem->get_contents($path);
        exit;
    }

    /**
     * Get the public URL for a file (via download token).
     */
    public function getFileUrl(array $file): string {
        return add_query_arg([
            'vollstart_rd_file' => $file['download_token'],
        ], home_url('/'));
    }

    // --- Thumbnail ---

    /**
     * Set a file as thumbnail (clears previous thumbnail for same entity).
     */
    public function setThumbnail(string $entityType, int $entityId, int $fileId): void {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        // Clear existing thumbnail
        $db->rawQuery("UPDATE %i SET is_thumbnail = 0 WHERE entity_type = %s AND entity_id = %d", $tbl, $entityType, $entityId);
        // Set new
        $db->update('files', ['is_thumbnail' => 1], ['id' => $fileId]);
    }

    /**
     * Get thumbnail file for an entity.
     * If no thumbnail is explicitly set, returns the first image.
     */
    public function getThumbnail(string $entityType, int $entityId): ?array {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        // Try explicit thumbnail first
        $thumb = $db->getRow("SELECT * FROM %i WHERE entity_type = %s AND entity_id = %d AND is_thumbnail = 1", $tbl, $entityType, $entityId);
        if ($thumb) return $thumb;
        // Fallback: first image
        return $db->getRow("SELECT * FROM %i WHERE entity_type = %s AND entity_id = %d AND mime_type LIKE 'image/%%' ORDER BY id ASC LIMIT 1", $tbl, $entityType, $entityId);
    }

    /**
     * Get thumbnail URL for an entity. Returns empty string if none.
     */
    public function getThumbnailUrl(string $entityType, int $entityId): string {
        $file = $this->getThumbnail($entityType, $entityId);
        return $file ? $this->getFileUrl($file) : '';
    }

    // --- Temp files ---

    /**
     * Reassign temp files to a real entity after creation.
     * Matches by temp_token, sets entity_type + entity_id, clears temp_token.
     */
    public function claimTempFiles(string $tempToken, string $entityType, int $entityId): int {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        return (int) $db->rawQuery(
            "UPDATE %i SET entity_type = %s, entity_id = %d, temp_token = '' WHERE temp_token = %s",
            $tbl, $entityType, $entityId, $tempToken
        );
    }

    /**
     * Delete all temp files for a session (on cancel).
     */
    public function deleteTempFiles(string $tempToken): void {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        $files = $db->getResults("SELECT * FROM %i WHERE temp_token = %s", $tbl, $tempToken);
        foreach ($files as $file) {
            $this->delete($file['id']);
        }
    }

    /**
     * Cleanup old temp files (older than threshold).
     * Called at the start of new repair form to avoid orphans.
     */
    public function cleanupOldTempFiles(int $maxAgeHours = 24): int {
        $db = $this->MAIN->getDB();
        $tbl = $db->getTabelle('files');
        $cutoff = wp_date('Y-m-d H:i:s', time() - ($maxAgeHours * 3600));

        $oldFiles = $db->getResults(
            "SELECT * FROM %i WHERE temp_token != '' AND created_at < %s",
            $tbl, $cutoff
        );

        foreach ($oldFiles as $file) {
            $this->delete($file['id']);
        }

        return count($oldFiles);
    }

    // --- Validation ---

    private function validateFile(array $fileData) {
        if (empty($fileData['tmp_name']) || !is_uploaded_file($fileData['tmp_name'])) {
            return new \WP_Error('no_file', __('No file uploaded.', 'vollstart-repair-desk'));
        }

        if ($fileData['error'] !== UPLOAD_ERR_OK) {
            return new \WP_Error('upload_error', __('Upload error.', 'vollstart-repair-desk'));
        }

        // Max 10MB
        if ($fileData['size'] > 10 * 1024 * 1024) {
            return new \WP_Error('too_large', __('File too large. Maximum 10 MB.', 'vollstart-repair-desk'));
        }

        $ext = '.' . strtolower(pathinfo($fileData['name'], PATHINFO_EXTENSION));
        foreach ($this->dangerousExtensions as $dangerous) {
            if ($ext === $dangerous) {
                return new \WP_Error('dangerous_extension', __('This file type is not allowed.', 'vollstart-repair-desk'));
            }
        }

        // Check file content for dangerous patterns
        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();
        $fullContent = $wp_filesystem->get_contents($fileData['tmp_name']);
        $content = ($fullContent !== false) ? substr($fullContent, 0, 200) : '';
        foreach ($this->dangerousContent as $pattern) {
            if (stripos($content, $pattern) !== false) {
                return new \WP_Error('dangerous_content', __('This file contains potentially dangerous content.', 'vollstart-repair-desk'));
            }
        }

        return true;
    }

    // --- Storage helpers ---

    public function getStorageDir(): string {
        $uploadDir = wp_upload_dir();
        return $uploadDir['basedir'] . '/repair-desk/files';
    }

    public function getFilePath(array $fileRow): string {
        return $this->getStorageDir() . '/' . $fileRow['storage_hash'] . '/' . $fileRow['stored_name'];
    }

    public function ensureHtaccess(): void {
        $dir = $this->getStorageDir();
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
        }

        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();

        $htaccess = $dir . '/.htaccess';
        if (!file_exists($htaccess)) {
            $wp_filesystem->put_contents($htaccess, "Order deny,allow\nDeny from all\n", FS_CHMOD_FILE);
        }

        $index = $dir . '/index.php';
        if (!file_exists($index)) {
            $wp_filesystem->put_contents($index, "<?php\n// Silence is golden.\n", FS_CHMOD_FILE);
        }
    }

    private function generateUniqueToken(): string {
        return bin2hex(random_bytes(32));
    }

    private function isDirectoryEmpty(string $dir): bool {
        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        WP_Filesystem();
        $list = $wp_filesystem->dirlist($dir);
        return empty($list);
    }
}
