api.php

77.80 KB
31/07/2025 13:09
PHP
api.php
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

// Include required classes
require_once 'classes/DataManager.php';
require_once 'classes/ImageProcessor.php';
require_once 'classes/FileUploadHandler.php';
require_once 'classes/RSSGenerator.php';
require_once 'classes/ErrorHandler.php';
require_once 'classes/InputValidator.php';
require_once 'classes/SimpleAuth.php';

class PhotoGalleryAPI
{
    /**
     * @var mixed
     */
    private $albumsPath;

    /**
     * @var DataManager
     */
    private $dataManager;

    /**
     * @var ErrorHandler
     */
    private $errorHandler;

    /**
     * @var InputValidator
     */
    private $inputValidator;

    /**
     * @var SimpleAuth
     */
    private $auth;

    public function __construct()
    {
        $this->albumsPath = __DIR__.'/albums/';
        $this->dataManager = new DataManager();
        $this->auth = new SimpleAuth();

        // Initialize error handling and validation
        $this->errorHandler = new ErrorHandler('logs/api_error.log', 'INFO', true);
        $this->inputValidator = new InputValidator($this->errorHandler);

        // Initialize data if needed
        try {
            $this->dataManager->initializeData();
        } catch (Exception $e) {
            // Use new error handler for logging
            $this->errorHandler->logError(
                'DataManager initialization failed: '.$e->getMessage(),
                'CONFIGURATION_ERROR',
                ['exception' => get_class($e)],
                ['method' => 'constructor']
            );
        }
    }

    /**
     * Handle incoming API requests with comprehensive error handling
     * @return string JSON response
     */
    public function handleRequest()
    {
        try {
            // Log incoming request for audit trail
            $this->errorHandler->logInfo('API Request', [
                'action' => $_GET['action'] ?? 'none',
                'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
            ]);

            $action = $_GET['action'] ?? '';
            $method = $_SERVER['REQUEST_METHOD'];

            // Validate action parameter
            if (empty($action)) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Action parameter is required',
                    'MISSING_REQUIRED_FIELD',
                    ['parameter' => 'action']
                );
                return $this->sendResponse($errorResponse);
            }

            // Define allowed actions and their required methods
            $allowedActions = [
                'getAlbums' => ['GET'],
                'getPhotos' => ['GET'],
                'createAlbum' => ['POST'],
                'updateAlbum' => ['POST'],
                'deleteAlbum' => ['POST'],
                'getTags' => ['GET'],
                'createTag' => ['POST'],
                'deleteTag' => ['POST'],
                'updateAlbumTags' => ['POST'],
                'uploadPhoto' => ['POST'],
                'deletePhoto' => ['POST'],
                'updateRSSSettings' => ['POST'],
                'getConfig' => ['GET'],
                'updateConfig' => ['POST'],
                'getStorageStats' => ['GET'],
                'cleanupOrphanedFiles' => ['POST'],
                'generateThumbnails' => ['POST'],
                'getAlbumStorageUsage' => ['GET'],
                'checkAuth' => ['GET']
            ];

            // Validate action exists
            if (!isset($allowedActions[$action])) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Invalid action: '.$action,
                    'INVALID_PARAMETER',
                    ['action' => $action, 'allowed_actions' => array_keys($allowedActions)]
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate HTTP method
            $allowedMethods = $allowedActions[$action];
            if (!in_array($method, $allowedMethods)) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    "Method {$method} not allowed for action {$action}. Allowed methods: ".implode(', ', $allowedMethods),
                    'METHOD_NOT_ALLOWED',
                    ['method' => $method, 'action' => $action, 'allowed_methods' => $allowedMethods]
                );
                return $this->sendResponse($errorResponse);
            }

            // Route to appropriate method
            switch ($action) {
                case 'getAlbums':
                    return $this->getAlbums();
                case 'getPhotos':
                    return $this->getPhotos();
                case 'createAlbum':
                    $this->auth->requireAuth();
                    return $this->createAlbum();
                case 'updateAlbum':
                    $this->auth->requireAuth();
                    return $this->updateAlbum();
                case 'deleteAlbum':
                    $this->auth->requireAuth();
                    return $this->deleteAlbum();
                case 'getTags':
                    return $this->getTags();
                case 'createTag':
                    $this->auth->requireAuth();
                    return $this->createTag();
                case 'deleteTag':
                    $this->auth->requireAuth();
                    return $this->deleteTag();
                case 'updateAlbumTags':
                    $this->auth->requireAuth();
                    return $this->updateAlbumTags();
                case 'uploadPhoto':
                    $this->auth->requireAuth();
                    return $this->uploadPhoto();
                case 'deletePhoto':
                    $this->auth->requireAuth();
                    return $this->deletePhoto();
                case 'updateRSSSettings':
                    $this->auth->requireAuth();
                    return $this->updateRSSSettings();
                case 'getConfig':
                    return $this->getConfig();
                case 'updateConfig':
                    $this->auth->requireAuth();
                    return $this->updateConfig();
                case 'getStorageStats':
                    $this->auth->requireAuth();
                    return $this->getStorageStats();
                case 'cleanupOrphanedFiles':
                    $this->auth->requireAuth();
                    return $this->cleanupOrphanedFiles();
                case 'generateThumbnails':
                    $this->auth->requireAuth();
                    return $this->generateThumbnails();
                case 'getAlbumStorageUsage':
                    $this->auth->requireAuth();
                    return $this->getAlbumStorageUsage();
                case 'checkAuth':
                    return $this->checkAuthStatus();
                default:
                    $errorResponse = $this->errorHandler->createErrorResponse(
                        'Invalid action: "'.$action.'" (length: '.strlen($action).')',
                        'INVALID_ACTION'
                    );
                    return $this->sendResponse($errorResponse);
            }

        } catch (Exception $e) {
            // Handle any unexpected exceptions
            $errorResponse = $this->errorHandler->handleException($e, 'handleRequest');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Create new album with enhanced validation and error handling
     * @return string JSON response
     */
    private function createAlbum()
    {
        try {
            // Get and validate JSON input
            $inputRaw = file_get_contents('php://input');
            if ($inputRaw === false) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to read request body',
                    'INVALID_INPUT'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate JSON format
            $input = json_decode($inputRaw, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Invalid JSON input: '.json_last_error_msg(),
                    'INVALID_JSON'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate album creation input
            $validationError = $this->inputValidator->validateAlbumCreation($input);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Sanitize input
            $title = trim($input['title']);
            $description = isset($input['description']) ? trim($input['description']) : '';

            // Create album in database
            $albumId = $this->dataManager->createAlbum($title, $description);

            // Create physical album directory
            $albumPath = $this->albumsPath.$albumId;
            if (!is_dir($albumPath)) {
                if (!mkdir($albumPath, 0755, true)) {
                    // Rollback database entry
                    try {
                        $this->dataManager->deleteAlbum($albumId);
                    } catch (Exception $rollbackError) {
                        $this->errorHandler->logError(
                            'Failed to rollback album creation: '.$rollbackError->getMessage(),
                            'DATABASE_ERROR',
                            ['album_id' => $albumId],
                            ['method' => 'createAlbum', 'operation' => 'rollback']
                        );
                    }

                    $errorResponse = $this->errorHandler->createErrorResponse(
                        'Failed to create album directory',
                        'FILE_SYSTEM_ERROR',
                        ['album_path' => $albumPath]
                    );
                    return $this->sendResponse($errorResponse);
                }
            }

            // Get the created album data
            $album = $this->dataManager->getAlbum($albumId);
            if (!$album) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album created but failed to retrieve data',
                    'DATABASE_ERROR',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Album created successfully',
                'album' => $album
            ], 201, 'Album created successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'createAlbum');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Update existing album with enhanced validation and error handling
     * @return string JSON response
     */
    private function updateAlbum()
    {
        try {
            // Validate album ID parameter
            $albumId = $_GET['albumId'] ?? '';
            $validationError = $this->inputValidator->validateAlbumId($albumId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Get and validate JSON input
            $inputRaw = file_get_contents('php://input');
            if ($inputRaw === false) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to read request body',
                    'INVALID_INPUT'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate JSON format
            $input = json_decode($inputRaw, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Invalid JSON input: '.json_last_error_msg(),
                    'INVALID_JSON'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate album update input
            $validationError = $this->inputValidator->validateAlbumUpdate($input);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Validate album exists
            $existingAlbum = $this->dataManager->getAlbum($albumId);
            if (!$existingAlbum) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album not found',
                    'ALBUM_NOT_FOUND',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            // Prepare sanitized update data
            $updateData = [];

            if (isset($input['title'])) {
                $updateData['title'] = trim($input['title']);
            }

            if (isset($input['description'])) {
                $updateData['description'] = trim($input['description']);
            }

            if (isset($input['is_rss_enabled'])) {
                $updateData['is_rss_enabled'] = (bool) $input['is_rss_enabled'];
            }

            // Update album
            $success = $this->dataManager->updateAlbum($albumId, $updateData);

            if (!$success) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to update album',
                    'DATABASE_ERROR',
                    ['album_id' => $albumId, 'update_data' => $updateData]
                );
                return $this->sendResponse($errorResponse);
            }

            // Get updated album data
            $album = $this->dataManager->getAlbum($albumId);

            // Trigger RSS feed auto-update if RSS settings were changed or album is RSS-enabled
            if (isset($updateData['is_rss_enabled']) || !empty($album['is_rss_enabled'])) {
                $this->triggerRSSAutoUpdate();
            }

            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Album updated successfully',
                'album' => $album
            ], 200, 'Album updated successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'updateAlbum');
            return $this->sendResponse($errorResponse);
        }
    }

/**
 * Delete a tag with enhanced validation and error handling
 * @return string JSON response
 */
    private function deleteTag()
    {
        try {
            // รับ tagId จากคำขอ
            $tagId = $_GET['tagId'] ?? '';

            // ตรวจสอบความถูกต้องของ tagId
            $validationError = $this->inputValidator->validateTagId($tagId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // ตรวจสอบว่า tag มีอยู่จริง
            $existingTag = $this->dataManager->getTag($tagId);
            if (!$existingTag) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Tag not found',
                    'TAG_NOT_FOUND',
                    ['tag_id' => $tagId]
                );
                return $this->sendResponse($errorResponse);
            }

            // ลบ tag ออกจากฐานข้อมูล
            $success = $this->dataManager->deleteTag($tagId);
            if (!$success) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to delete tag',
                    'DATABASE_ERROR',
                    ['tag_id' => $tagId]
                );
                return $this->sendResponse($errorResponse);
            }

            // ส่งคำตอบสำเร็จ
            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Tag deleted successfully',
                'tag_id' => $tagId
            ], 200, 'Tag deleted successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            // จัดการข้อผิดพลาดที่ไม่คาดคิด
            $errorResponse = $this->errorHandler->handleException($e, 'deleteTag');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Delete album and cleanup photos with enhanced validation and error handling
     * @return string JSON response
     */
    private function deleteAlbum()
    {
        try {
            // Validate album ID parameter
            $albumId = $_GET['albumId'] ?? '';
            $validationError = $this->inputValidator->validateAlbumId($albumId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Validate album exists
            $existingAlbum = $this->dataManager->getAlbum($albumId);
            if (!$existingAlbum) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album not found',
                    'ALBUM_NOT_FOUND',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            // Delete physical album directory and all photos
            $albumPath = $this->albumsPath.$albumId;
            if (is_dir($albumPath)) {
                if (!$this->deleteDirectory($albumPath)) {
                    $this->errorHandler->logWarning(
                        'Failed to delete album directory completely',
                        ['album_id' => $albumId, 'album_path' => $albumPath]
                    );
                }
            }

            // Delete album from database
            $success = $this->dataManager->deleteAlbum($albumId);

            if (!$success) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to delete album from database',
                    'DATABASE_ERROR',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Album deleted successfully',
                'albumId' => $albumId
            ], 200, 'Album deleted successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'deleteAlbum');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Get all available tags with enhanced error handling
     * @return string JSON response
     */
    private function getTags()
    {
        try {
            $tags = $this->dataManager->getTags();

            $successResponse = $this->errorHandler->createSuccessResponse([
                'tags' => $tags
            ], 200, 'Tags retrieved successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'getTags');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Create new tag with enhanced validation and error handling
     * @return string JSON response
     */
    private function createTag()
    {
        try {
            // Get and validate JSON input
            $inputRaw = file_get_contents('php://input');
            if ($inputRaw === false) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to read request body',
                    'INVALID_INPUT'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate JSON format
            $input = json_decode($inputRaw, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Invalid JSON input: '.json_last_error_msg(),
                    'INVALID_JSON'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate tag creation input
            $validationError = $this->inputValidator->validateTagCreation($input);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Sanitize input
            $name = trim($input['name']);
            $color = isset($input['color']) ? trim($input['color']) : '#3498db';

            // Create tag in database
            $tagId = $this->dataManager->createTag($name, $color);

            // Get the created tag data
            $tags = $this->dataManager->getTags();
            $createdTag = null;
            foreach ($tags as $tag) {
                if ($tag['id'] == $tagId) {
                    $createdTag = $tag;
                    break;
                }
            }

            if (!$createdTag) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Tag created but failed to retrieve data',
                    'DATABASE_ERROR',
                    ['tag_id' => $tagId]
                );
                return $this->sendResponse($errorResponse);
            }

            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Tag created successfully',
                'tag' => $createdTag
            ], 201, 'Tag created successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'createTag');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Update album tags with enhanced validation and error handling
     * @return string JSON response
     */
    private function updateAlbumTags()
    {
        try {
            // Validate album ID parameter
            $albumId = $_GET['albumId'] ?? '';
            $validationError = $this->inputValidator->validateAlbumId($albumId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Get and validate JSON input
            $inputRaw = file_get_contents('php://input');
            if ($inputRaw === false) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to read request body',
                    'INVALID_INPUT'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate JSON format
            $input = json_decode($inputRaw, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Invalid JSON input: '.json_last_error_msg(),
                    'INVALID_JSON'
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate album tags update input
            $validationError = $this->inputValidator->validateAlbumTagsUpdate($input);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Validate album exists
            $existingAlbum = $this->dataManager->getAlbum($albumId);
            if (!$existingAlbum) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album not found',
                    'ALBUM_NOT_FOUND',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            $tagIds = $input['tagIds'];

            // Validate all tag IDs exist
            $allTags = $this->dataManager->getTags();
            $validTagIds = array_column($allTags, 'id');

            foreach ($tagIds as $tagId) {
                if (!in_array((int) $tagId, $validTagIds)) {
                    $errorResponse = $this->errorHandler->createErrorResponse(
                        'Invalid tag ID: '.$tagId,
                        'TAG_NOT_FOUND',
                        ['tag_id' => $tagId, 'valid_tag_ids' => $validTagIds]
                    );
                    return $this->sendResponse($errorResponse);
                }
            }

            // Update album tags
            $success = $this->dataManager->updateAlbumTags($albumId, $tagIds);

            if (!$success) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to update album tags',
                    'DATABASE_ERROR',
                    ['album_id' => $albumId, 'tag_ids' => $tagIds]
                );
                return $this->sendResponse($errorResponse);
            }

            // Get updated album data with tags
            $album = $this->dataManager->getAlbum($albumId);
            $albumTags = $this->dataManager->getAlbumTags($albumId);

            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Album tags updated successfully',
                'album' => $album,
                'tags' => $albumTags
            ], 200, 'Album tags updated successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'updateAlbumTags');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Upload photos to album with enhanced validation and error handling
     * @return string JSON response
     */
    private function uploadPhoto()
    {
        try {
            // Validate album ID and file upload parameters
            $albumId = $_GET['albumId'] ?? '';
            $validationError = $this->inputValidator->validateFileUpload($_FILES, $albumId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Validate album exists
            $existingAlbum = $this->dataManager->getAlbum($albumId);
            if (!$existingAlbum) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album not found',
                    'ALBUM_NOT_FOUND',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            // Get configuration for upload processing
            $config = $this->dataManager->getAllConfig();

            // Initialize image processor and file upload handler
            $imageProcessor = new ImageProcessor($config);
            $uploadHandler = new FileUploadHandler($config, $imageProcessor, $this->dataManager);

            // Check storage limits before processing
            $totalUploadSize = 0;
            $files = $_FILES['photos'];

            // Calculate total size of files to be uploaded
            if (is_array($files['size'])) {
                $totalUploadSize = array_sum($files['size']);
            } else {
                $totalUploadSize = $files['size'];
            }

            $storageCheck = $uploadHandler->checkStorageLimit($totalUploadSize);
            if (!$storageCheck['can_upload']) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Storage limit exceeded. Available space: '.$this->formatBytes($storageCheck['available_space'] ?? 0),
                    'STORAGE_FULL',
                    [
                        'available_space' => $storageCheck['available_space'] ?? 0,
                        'requested_space' => $totalUploadSize,
                        'current_usage' => $storageCheck['current_usage'] ?? 0
                    ]
                );
                return $this->sendResponse($errorResponse);
            }

            // Process file uploads
            $uploadResult = $uploadHandler->handleUpload($albumId, $files);

            if (!$uploadResult['success']) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Upload failed: '.$uploadResult['error'],
                    'IMAGE_PROCESSING_ERROR',
                    ['upload_result' => $uploadResult]
                );
                return $this->sendResponse($errorResponse);
            }

            // Prepare response with detailed results
            $response = [
                'message' => 'Photos uploaded successfully',
                'album_id' => $albumId,
                'total_files' => $uploadResult['total_files'],
                'successful_uploads' => $uploadResult['successful_uploads'],
                'failed_uploads' => $uploadResult['total_files'] - $uploadResult['successful_uploads'],
                'upload_results' => $uploadResult['results']
            ];

            // Add storage information
            if (isset($storageCheck['current_usage'])) {
                $response['storage_info'] = [
                    'current_usage' => $storageCheck['current_usage'],
                    'max_size' => $storageCheck['max_size'] ?? null,
                    'usage_percentage' => $storageCheck['usage_percentage'] ?? null
                ];
            }

            // Get updated album information
            $updatedAlbum = $this->dataManager->getAlbum($albumId);
            $response['album'] = $updatedAlbum;

            // Trigger RSS feed auto-update if album is RSS-enabled
            if (!empty($updatedAlbum['is_rss_enabled'])) {
                $this->triggerRSSAutoUpdate();
            }

            $successResponse = $this->errorHandler->createSuccessResponse(
                $response,
                200,
                'Photos uploaded successfully'
            );

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'uploadPhoto');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Delete a single photo from an album
     * @return string JSON response
     */
    private function deletePhoto()
    {
        try {
            // Get parameters
            $albumId = $_GET['albumId'] ?? '';
            $photoFilename = $_GET['filename'] ?? '';

            // Validate album ID
            $validationError = $this->inputValidator->validateAlbumId($albumId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Validate filename
            if (empty($photoFilename)) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Photo filename is required',
                    'MISSING_REQUIRED_FIELD',
                    ['field' => 'filename']
                );
                return $this->sendResponse($errorResponse);
            }

            // Validate album exists
            $existingAlbum = $this->dataManager->getAlbum($albumId);
            if (!$existingAlbum) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album not found',
                    'ALBUM_NOT_FOUND',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            // Get album photos to verify photo exists
            $photos = $this->dataManager->getPhotos($albumId);
            $photoToDelete = null;

            foreach ($photos as $photo) {
                if ($photo['filename'] === $photoFilename) {
                    $photoToDelete = $photo;
                    break;
                }
            }

            if (!$photoToDelete) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Photo not found in album',
                    'PHOTO_NOT_FOUND',
                    ['album_id' => $albumId, 'filename' => $photoFilename]
                );
                return $this->sendResponse($errorResponse);
            }

            // Delete physical file
            $photoPath = $this->albumsPath.$albumId.'/'.$photoFilename;
            if (file_exists($photoPath)) {
                if (!unlink($photoPath)) {
                    $errorResponse = $this->errorHandler->createErrorResponse(
                        'Failed to delete photo file',
                        'FILE_SYSTEM_ERROR',
                        ['photo_path' => $photoPath]
                    );
                    return $this->sendResponse($errorResponse);
                }
            }

            // Remove photo from database
            $success = $this->dataManager->deletePhotoByFilename($albumId, $photoFilename);

            if (!$success) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to delete photo from database',
                    'DATABASE_ERROR',
                    ['album_id' => $albumId, 'filename' => $photoFilename]
                );
                return $this->sendResponse($errorResponse);
            }

            $successResponse = $this->errorHandler->createSuccessResponse([
                'message' => 'Photo deleted successfully',
                'albumId' => $albumId,
                'filename' => $photoFilename
            ], 200, 'Photo deleted successfully');

            return $this->sendResponse($successResponse);

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'deletePhoto');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Recursively delete directory and all contents
     * @param string $dir Directory path
     * @return bool Success status
     */
    private function deleteDirectory($dir)
    {
        if (!is_dir($dir)) {
            return false;
        }

        $files = array_diff(scandir($dir), ['.', '..']);

        foreach ($files as $file) {
            $path = $dir.'/'.$file;
            if (is_dir($path)) {
                $this->deleteDirectory($path);
            } else {
                unlink($path);
            }
        }

        return rmdir($dir);
    }

    /**
     * Get all albums with enhanced metadata including title, description, tags, and photo count
     * @return mixed JSON response with albums data
     */
    private function getAlbums()
    {
        try {
            // Get query parameters for filtering and pagination
            $tagFilter = $_GET['tag'] ?? '';

            // Input validation for tag filter
            if (!empty($tagFilter) && !is_numeric($tagFilter)) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Invalid tag filter. Tag ID must be numeric.',
                    'VALIDATION_FAILED',
                    ['tag_filter' => $tagFilter]
                );
                return $this->sendResponse($errorResponse);
            }

            // Try to get albums from DataManager first
            try {
                $dbAlbums = $this->dataManager->getAlbums(true);

                // Enhance each album with complete metadata
                foreach ($dbAlbums as &$album) {
                    // Extract folder number from title (e.g., "Album 12" -> "12")
                    $folderNumber = $album['id']; // Default to id
                    if (preg_match('/Album (\d+)/', $album['title'], $matches)) {
                        $folderNumber = $matches[1];
                    }

                    // Get photos from filesystem to ensure accuracy
                    $filesystemPhotos = $this->getAlbumPhotos($folderNumber);
                    $dbPhotos = $this->dataManager->getPhotos($album['id']);

                    // Use database photos if available, otherwise fallback to filesystem
                    $photos = !empty($dbPhotos) ? $dbPhotos : array_map(function ($filename) use ($album) {
                        return [
                            'id' => null,
                            'filename' => $filename,
                            'original_filename' => $filename,
                            'file_size' => null,
                            'width' => null,
                            'height' => null,
                            'uploaded_at' => null
                        ];
                    }, $filesystemPhotos);

                    // Set cover photo (first uploaded image)
                    $coverPhoto = null;
                    if (!empty($photos)) {
                        $coverPhoto = [
                            'filename' => $photos[0]['filename'],
                            'path' => "albums/{$folderNumber}/{$photos[0]['filename']}"
                        ];
                    }

                    // Ensure all required fields are present
                    $album['title'] = $album['title'] ?? "Album ".$album['id'];
                    $album['description'] = $album['description'] ?? '';
                    $album['photo_count'] = count($photos);
                    $album['photoCount'] = $album['photo_count']; // Backward compatibility
                    $album['name'] = $album['title']; // Backward compatibility
                    $album['cover_photo'] = $coverPhoto;
                    $album['photos'] = !empty($photos) ? [$photos[0]] : []; // First photo for backward compatibility

                    // Get detailed tag information
                    $albumTags = $this->dataManager->getAlbumTags($album['id']);
                    $album['tag_details'] = $albumTags;

                    // Ensure tags array exists
                    $album['tags'] = $album['tags'] ?? [];

                    // Add timestamps if missing
                    $album['created_at'] = $album['created_at'] ?? date('c');
                    $album['updated_at'] = $album['updated_at'] ?? date('c');
                    $album['is_rss_enabled'] = $album['is_rss_enabled'] ?? false;
                }

                // Apply tag filtering if specified
                if (!empty($tagFilter)) {
                    $dbAlbums = array_filter($dbAlbums, function ($album) use ($tagFilter) {
                        return in_array((int) $tagFilter, $album['tags']);
                    });
                }

                // Re-index array after filtering and sort by updated_at descending
                $dbAlbums = array_values($dbAlbums);
                usort($dbAlbums, function ($a, $b) {
                    return strtotime($b['updated_at']) - strtotime($a['updated_at']);
                });

                return $this->success([
                    'albums' => $dbAlbums,
                    'total_count' => count($dbAlbums),
                    'filtered_by_tag' => !empty($tagFilter) ? (int) $tagFilter : null
                ]);

            } catch (Exception $e) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Failed to load albums from database: '.$e->getMessage(),
                    'DATABASE_ERROR',
                    ['error_details' => $e->getMessage()]
                );
                return $this->sendResponse($errorResponse);
            }

        } catch (Exception $e) {
            $errorResponse = $this->errorHandler->handleException($e, 'getAlbums');
            return $this->sendResponse($errorResponse);
        }
    }

    /**
     * Get photos for album with enhanced metadata and pagination
     * @return mixed JSON response with photos data
     */
    private function getPhotos()
    {
        try {
            // Get and validate query parameters
            $albumId = $_GET['albumId'] ?? '';
            $page = (int) ($_GET['page'] ?? 1);
            $limit = (int) ($_GET['limit'] ?? 20);
            $sortBy = $_GET['sortBy'] ?? 'uploaded_at'; // uploaded_at, filename, file_size
            $sortOrder = $_GET['sortOrder'] ?? 'desc'; // asc, desc

            // Validate album ID parameter
            $validationError = $this->inputValidator->validateAlbumId($albumId);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Validate pagination parameters
            $paginationParams = [
                'page' => $page,
                'limit' => $limit,
                'sortBy' => $sortBy,
                'sortOrder' => $sortOrder
            ];
            $validationError = $this->inputValidator->validatePaginationParams($paginationParams);
            if ($validationError) {
                return $this->sendResponse($validationError);
            }

            // Get album info to determine correct folder
            $album = $this->dataManager->getAlbum($albumId);
            if (!$album) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album not found in database',
                    'ALBUM_NOT_FOUND',
                    ['album_id' => $albumId]
                );
                return $this->sendResponse($errorResponse);
            }

            // Extract folder number from title (e.g., "Album 12" -> "12")
            $folderNumber = $albumId; // Default to albumId
            if (preg_match('/Album (\d+)/', $album['title'], $matches)) {
                $folderNumber = $matches[1];
            }

            // Validate album folder exists
            $albumPath = $this->albumsPath.$folderNumber.'/';
            if (!is_dir($albumPath)) {
                $errorResponse = $this->errorHandler->createErrorResponse(
                    'Album folder not found',
                    'ALBUM_FOLDER_NOT_FOUND',
                    ['album_id' => $albumId, 'folder_number' => $folderNumber, 'path' => $albumPath]
                );
                return $this->sendResponse($errorResponse);
            }

            // Try to get photos from database first (with metadata)
            try {
                $dbPhotos = $this->dataManager->getPhotos($albumId);

                if (!empty($dbPhotos)) {
                    // Enhance photos with additional metadata and file system verification
                    $enhancedPhotos = [];

                    foreach ($dbPhotos as $photo) {
                        $filePath = $albumPath.$photo['filename'];

                        // Verify file still exists on filesystem
                        if (!file_exists($filePath)) {
                            continue; // Skip missing files
                        }

                        // Get file system metadata
                        $fileSize = filesize($filePath);
                        $fileModTime = filemtime($filePath);

                        // Get image dimensions if not stored in database
                        $width = $photo['width'];
                        $height = $photo['height'];

                        if (empty($width) || empty($height)) {
                            $imageInfo = getimagesize($filePath);
                            if ($imageInfo !== false) {
                                $width = $imageInfo[0];
                                $height = $imageInfo[1];
                            }
                        }

                        $enhancedPhoto = [
                            'id' => $photo['id'],
                            'filename' => $photo['filename'],
                            'original_filename' => $photo['original_filename'] ?? $photo['filename'],
                            'file_size' => $photo['file_size'] ?? $fileSize,
                            'file_size_formatted' => $this->formatBytes($photo['file_size'] ?? $fileSize),
                            'width' => $width,
                            'height' => $height,
                            'aspect_ratio' => ($width && $height) ? round($width / $height, 2) : null,
                            'uploaded_at' => $photo['uploaded_at'],
                            'uploaded_at_formatted' => $photo['uploaded_at'] ? date('Y-m-d H:i:s', strtotime($photo['uploaded_at'])) : null,
                            'file_modified_at' => date('c', $fileModTime),
                            'path' => "albums/{$albumId}/{$photo['filename']}",
                            'thumbnail_path' => "albums/{$albumId}/thumb_{$photo['filename']}",
                            'has_thumbnail' => file_exists($albumPath.'thumb_'.$photo['filename'])
                        ];

                        $enhancedPhotos[] = $enhancedPhoto;
                    }

                    // Sort photos based on parameters
                    usort($enhancedPhotos, function ($a, $b) use ($sortBy, $sortOrder) {
                        $valueA = $a[$sortBy] ?? '';
                        $valueB = $b[$sortBy] ?? '';

                        // Handle different data types
                        if (in_array($sortBy, ['file_size', 'width', 'height'])) {
                            $valueA = (int) $valueA;
                            $valueB = (int) $valueB;
                        } elseif ($sortBy === 'uploaded_at') {
                            $valueA = strtotime($valueA ?: '1970-01-01');
                            $valueB = strtotime($valueB ?: '1970-01-01');
                        }

                        $comparison = $valueA <=> $valueB;
                        return $sortOrder === 'desc' ? -$comparison : $comparison;
                    });

                    // Apply pagination
                    $totalPhotos = count($enhancedPhotos);
                    $offset = ($page - 1) * $limit;
                    $paginatedPhotos = array_slice($enhancedPhotos, $offset, $limit);

                    return $this->success([
                        'photos' => $paginatedPhotos,
                        'pagination' => [
                            'page' => $page,
                            'limit' => $limit,
                            'total_photos' => $totalPhotos,
                            'total_pages' => ceil($totalPhotos / $limit),
                            'has_more' => $offset + $limit < $totalPhotos,
                            'has_previous' => $page > 1
                        ],
                        'sorting' => [
                            'sort_by' => $sortBy,
                            'sort_order' => $sortOrder
                        ],
                        'album' => [
                            'id' => $album['id'],
                            'title' => $album['title'],
                            'photo_count' => $totalPhotos
                        ]
                    ]);
                }
            } catch (Exception $e) {
                // Log error but continue with filesystem fallback
                error_log('DataManager getPhotos error: '.$e->getMessage());
            }

            // Fallback to filesystem-based photo discovery with basic metadata
            $filesystemPhotos = $this->getAlbumPhotos($albumId);
            $enhancedPhotos = [];

            foreach ($filesystemPhotos as $filename) {
                $filePath = $albumPath.$filename;

                if (!file_exists($filePath)) {
                    continue;
                }

                $fileSize = filesize($filePath);
                $fileModTime = filemtime($filePath);

                // Get image dimensions
                $width = null;
                $height = null;
                $imageInfo = getimagesize($filePath);
                if ($imageInfo !== false) {
                    $width = $imageInfo[0];
                    $height = $imageInfo[1];
                }

                $enhancedPhoto = [
                    'id' => null, // No database ID for filesystem-only photos
                    'filename' => $filename,
                    'original_filename' => $filename,
                    'file_size' => $fileSize,
                    'file_size_formatted' => $this->formatBytes($fileSize),
                    'width' => $width,
                    'height' => $height,
                    'aspect_ratio' => ($width && $height) ? round($width / $height, 2) : null,
                    'uploaded_at' => null,
                    'uploaded_at_formatted' => null,
                    'file_modified_at' => date('c', $fileModTime),
                    'path' => "albums/{$albumId}/{$filename}",
                    'thumbnail_path' => "albums/{$albumId}/thumb_{$filename}",
                    'has_thumbnail' => file_exists($albumPath.'thumb_'.$filename)
                ];

                $enhancedPhotos[] = $enhancedPhoto;
            }

            // Sort photos (filesystem fallback uses file modification time for uploaded_at)
            usort($enhancedPhotos, function ($a, $b) use ($sortBy, $sortOrder) {
                $valueA = $a[$sortBy] ?? '';
                $valueB = $b[$sortBy] ?? '';

                // Handle sorting by uploaded_at for filesystem photos
                if ($sortBy === 'uploaded_at') {
                    $valueA = strtotime($a['file_modified_at']);
                    $valueB = strtotime($b['file_modified_at']);
                } elseif (in_array($sortBy, ['file_size', 'width', 'height'])) {
                    $valueA = (int) $valueA;
                    $valueB = (int) $valueB;
                }

                $comparison = $valueA <=> $valueB;
                return $sortOrder === 'desc' ? -$comparison : $comparison;
            });

            // Apply pagination
            $totalPhotos = count($enhancedPhotos);
            $offset = ($page - 1) * $limit;
            $paginatedPhotos = array_slice($enhancedPhotos, $offset, $limit);

            return $this->success([
                'photos' => $paginatedPhotos,
                'pagination' => [
                    'page' => $page,
                    'limit' => $limit,
                    'total_photos' => $totalPhotos,
                    'total_pages' => ceil($totalPhotos / $limit),
                    'has_more' => $offset + $limit < $totalPhotos,
                    'has_previous' => $page > 1
                ],
                'sorting' => [
                    'sort_by' => $sortBy,
                    'sort_order' => $sortOrder
                ],
                'album' => [
                    'id' => $albumId,
                    'title' => "Album {$albumId}",
                    'photo_count' => $totalPhotos
                ]
            ]);

        } catch (Exception $e) {
            return $this->error('Error loading photos: '.$e->getMessage());
        }
    }

    /**
     * @param $albumId
     * @return mixed
     */
    private function getAlbumPhotos($albumId)
    {
        $albumPath = $this->albumsPath.$albumId.'/';
        $photos = [];

        if (!is_dir($albumPath)) {
            return $photos;
        }

        $files = scandir($albumPath);

        foreach ($files as $file) {
            // Skip directories, thumbnail files, and non-image files
            if ($file === '.' || $file === '..' ||
                is_dir($albumPath.$file) ||
                strpos($file, 'thumb_') === 0 ||
                !$this->isImageFile($file)) {
                continue;
            }

            $photos[] = $file;
        }

        // Sort photos naturally (1.jpg, 2.jpg, 10.jpg, etc.)
        natsort($photos);
        return array_values($photos);
    }

    /**
     * @param $filename
     */
    private function isImageFile($filename)
    {
        $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'];
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        return in_array($extension, $allowedExtensions);
    }

    /**
     * Return success response with proper structure
     * @param array $data Response data
     * @param int $httpCode HTTP status code
     * @return string JSON response
     */
    private function success($data, $httpCode = 200)
    {
        http_response_code($httpCode);

        // Ensure consistent response structure
        $response = [
            'success' => true,
            'timestamp' => date('c'),
            'data' => $data
        ];

        // Merge data at root level for backward compatibility
        return json_encode($response + $data);
    }

    /**
     * Return error response with proper structure and logging
     * @param string $message Error message
     * @param int $httpCode HTTP status code
     * @param string $errorCode Internal error code
     * @param array $details Additional error details
     * @return string JSON response
     */
    private function error($message, $httpCode = 400, $errorCode = null, $details = [])
    {
        http_response_code($httpCode);

        // Log error for debugging
        $logMessage = "API Error: {$message}";
        if ($errorCode) {
            $logMessage .= " (Code: {$errorCode})";
        }
        if (!empty($details)) {
            $logMessage .= " Details: ".json_encode($details);
        }
        error_log($logMessage);

        $response = [
            'success' => false,
            'error' => $message,
            'timestamp' => date('c')
        ];

        if ($errorCode) {
            $response['error_code'] = $errorCode;
        }

        if (!empty($details)) {
            $response['details'] = $details;
        }

        return json_encode($response);
    }

    /**
     * Format bytes to human readable format
     * @param int $bytes Number of bytes
     * @return string Formatted string
     */
    private function formatBytes($bytes)
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);

        $bytes /= pow(1024, $pow);

        return round($bytes, 2).' '.$units[$pow];
    }

    /**
     * Update RSS configuration settings
     * @return mixed JSON response
     */
    private function updateRSSSettings()
    {
        try {
            // Get JSON input
            $input = json_decode(file_get_contents('php://input'), true);

            if (!$input) {
                return $this->error('Invalid JSON input');
            }

            // Define allowed RSS configuration keys
            $allowedKeys = [
                'rss_title',
                'rss_description',
                'rss_max_items',
                'rss_base_url'
            ];

            $updatedSettings = [];

            foreach ($input as $key => $value) {
                if (in_array($key, $allowedKeys)) {
                    // Validate specific settings
                    switch ($key) {
                        case 'rss_title':
                        case 'rss_description':
                            $value = trim($value);
                            if (empty($value)) {
                                return $this->error("$key cannot be empty");
                            }
                            break;

                        case 'rss_max_items':
                            $value = (int) $value;
                            if ($value < 1 || $value > 200) {
                                return $this->error('rss_max_items must be between 1 and 200');
                            }
                            break;

                        case 'rss_base_url':
                            $value = trim($value);
                            if (empty($value) || !filter_var($value, FILTER_VALIDATE_URL)) {
                                return $this->error('rss_base_url must be a valid URL');
                            }
                            break;
                    }

                    // Update configuration
                    $this->dataManager->setConfig($key, $value);
                    $updatedSettings[$key] = $value;
                }
            }

            if (empty($updatedSettings)) {
                return $this->error('No valid RSS settings provided');
            }

            return $this->success([
                'message' => 'RSS settings updated successfully',
                'updated_settings' => $updatedSettings
            ]);

        } catch (Exception $e) {
            return $this->error('Failed to update RSS settings: '.$e->getMessage());
        }
    }

    /**
     * Get configuration settings
     * @return mixed JSON response with configuration data
     */
    private function getConfig()
    {
        try {
            // Get query parameter to filter specific config keys
            $keys = $_GET['keys'] ?? '';
            $includeStorageInfo = isset($_GET['includeStorageInfo']) ? (bool) $_GET['includeStorageInfo'] : true;

            // Get all configuration
            $config = $this->dataManager->getAllConfig();

            // Filter by specific keys if requested
            if (!empty($keys)) {
                $requestedKeys = array_map('trim', explode(',', $keys));
                $filteredConfig = [];

                foreach ($requestedKeys as $key) {
                    if (array_key_exists($key, $config)) {
                        $filteredConfig[$key] = $config[$key];
                    }
                }

                $config = $filteredConfig;
            }

            // Add storage usage information if requested
            $storageInfo = null;
            if ($includeStorageInfo) {
                $storageInfo = $this->getStorageUsageInfo();
            }

            // Organize configuration into logical groups
            $organizedConfig = [
                'upload' => [
                    'max_file_size' => $config['upload_max_file_size'] ?? 10485760,
                    'max_file_size_formatted' => $this->formatBytes($config['upload_max_file_size'] ?? 10485760),
                    'allowed_types' => $config['upload_allowed_types'] ?? []
                ],
                'image_processing' => [
                    'webp_quality' => $config['image_webp_quality'] ?? 80,
                    'max_width' => $config['image_max_width'] ?? 1000,
                    'max_height' => $config['image_max_height'] ?? 1000,
                    'thumbnail_size' => $config['image_thumbnail_size'] ?? 300
                ],
                'rss' => [
                    'title' => $config['rss_title'] ?? 'Photo Gallery RSS Feed',
                    'description' => $config['rss_description'] ?? 'Latest photos from selected albums',
                    'max_items' => $config['rss_max_items'] ?? 50,
                    'base_url' => $config['rss_base_url'] ?? $this->getBaseUrl()
                ],
                'storage' => [
                    'max_total_size' => $config['storage_max_total_size'] ?? 1073741824,
                    'max_total_size_formatted' => $this->formatBytes($config['storage_max_total_size'] ?? 1073741824),
                    'cleanup_enabled' => $config['storage_cleanup_enabled'] ?? true
                ]
            ];

            $response = [
                'config' => $organizedConfig,
                'raw_config' => $config
            ];

            if ($storageInfo) {
                $response['storage_info'] = $storageInfo;
            }

            return $this->success($response);

        } catch (Exception $e) {
            return $this->error('Failed to get configuration: '.$e->getMessage());
        }
    }

    /**
     * Update configuration settings
     * @return mixed JSON response
     */
    private function updateConfig()
    {
        try {
            // Get JSON input
            $inputRaw = file_get_contents('php://input');
            if ($inputRaw === false) {
                return $this->error('Failed to read request body', 400, 'INPUT_READ_ERROR');
            }

            $input = json_decode($inputRaw, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                return $this->error('Invalid JSON input: '.json_last_error_msg(), 400, 'INVALID_JSON');
            }

            if (!is_array($input)) {
                return $this->error('Request body must be a JSON object', 400, 'INVALID_INPUT_TYPE');
            }

            // Define allowed configuration keys with validation rules
            $allowedKeys = [
                'upload_max_file_size' => [
                    'type' => 'integer',
                    'min' => 1048576, // 1MB minimum
                    'max' => 104857600, // 100MB maximum
                    'description' => 'Maximum file size for uploads (bytes)'
                ],
                'upload_allowed_types' => [
                    'type' => 'array',
                    'allowed_values' => ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp'],
                    'description' => 'Allowed MIME types for uploads'
                ],
                'image_webp_quality' => [
                    'type' => 'integer',
                    'min' => 1,
                    'max' => 100,
                    'description' => 'WebP compression quality (1-100)'
                ],
                'image_max_width' => [
                    'type' => 'integer',
                    'min' => 100,
                    'max' => 5000,
                    'description' => 'Maximum image width in pixels'
                ],
                'image_max_height' => [
                    'type' => 'integer',
                    'min' => 100,
                    'max' => 5000,
                    'description' => 'Maximum image height in pixels'
                ],
                'image_thumbnail_size' => [
                    'type' => 'integer',
                    'min' => 50,
                    'max' => 500,
                    'description' => 'Thumbnail size in pixels'
                ],
                'rss_title' => [
                    'type' => 'string',
                    'max_length' => 255,
                    'description' => 'RSS feed title'
                ],
                'rss_description' => [
                    'type' => 'string',
                    'max_length' => 1000,
                    'description' => 'RSS feed description'
                ],
                'rss_max_items' => [
                    'type' => 'integer',
                    'min' => 1,
                    'max' => 200,
                    'description' => 'Maximum items in RSS feed'
                ],
                'rss_base_url' => [
                    'type' => 'url',
                    'description' => 'Base URL for RSS feed links'
                ],
                'storage_max_total_size' => [
                    'type' => 'integer',
                    'min' => 104857600, // 100MB minimum
                    'max' => 10737418240, // 10GB maximum
                    'description' => 'Maximum total storage size (bytes)'
                ],
                'storage_cleanup_enabled' => [
                    'type' => 'boolean',
                    'description' => 'Enable automatic cleanup of orphaned files'
                ]
            ];

            $updatedSettings = [];
            $validationErrors = [];

            foreach ($input as $key => $value) {
                if (!array_key_exists($key, $allowedKeys)) {
                    $validationErrors[] = "Unknown configuration key: $key";
                    continue;
                }

                $rules = $allowedKeys[$key];

                // Validate based on type
                switch ($rules['type']) {
                    case 'integer':
                        if (!is_numeric($value)) {
                            $validationErrors[] = "$key must be a number";
                            continue 2;
                        }
                        $value = (int) $value;
                        if (isset($rules['min']) && $value < $rules['min']) {
                            $validationErrors[] = "$key must be at least {$rules['min']}";
                            continue 2;
                        }
                        if (isset($rules['max']) && $value > $rules['max']) {
                            $validationErrors[] = "$key must not exceed {$rules['max']}";
                            continue 2;
                        }
                        break;

                    case 'string':
                        if (!is_string($value)) {
                            $validationErrors[] = "$key must be a string";
                            continue 2;
                        }
                        $value = trim($value);
                        if (empty($value)) {
                            $validationErrors[] = "$key cannot be empty";
                            continue 2;
                        }
                        if (isset($rules['max_length']) && strlen($value) > $rules['max_length']) {
                            $validationErrors[] = "$key cannot exceed {$rules['max_length']} characters";
                            continue 2;
                        }
                        break;

                    case 'boolean':
                        $value = (bool) $value;
                        break;

                    case 'array':
                        if (!is_array($value)) {
                            $validationErrors[] = "$key must be an array";
                            continue 2;
                        }
                        if (isset($rules['allowed_values'])) {
                            foreach ($value as $item) {
                                if (!in_array($item, $rules['allowed_values'])) {
                                    $validationErrors[] = "$key contains invalid value: $item";
                                    continue 3;
                                }
                            }
                        }
                        break;

                    case 'url':
                        if (!is_string($value)) {
                            $validationErrors[] = "$key must be a string";
                            continue 2;
                        }
                        $value = trim($value);
                        if (empty($value) || !filter_var($value, FILTER_VALIDATE_URL)) {
                            $validationErrors[] = "$key must be a valid URL";
                            continue 2;
                        }
                        break;
                }

                // Additional validation for storage limits
                if ($key === 'storage_max_total_size') {
                    $currentUsage = $this->getStorageUsageInfo();
                    if ($currentUsage && isset($currentUsage['current_usage']) && $value < $currentUsage['current_usage']) {
                        $validationErrors[] = "storage_max_total_size cannot be less than current usage ({$this->formatBytes($currentUsage['current_usage'])})";
                        continue;
                    }
                }

                // Update configuration
                $this->dataManager->setConfig($key, $value);
                $updatedSettings[$key] = $value;
            }

            if (!empty($validationErrors)) {
                return $this->error('Configuration validation failed', 400, 'VALIDATION_ERROR', [
                    'validation_errors' => $validationErrors
                ]);
            }

            if (empty($updatedSettings)) {
                return $this->error('No valid configuration settings provided', 400, 'NO_VALID_SETTINGS');
            }

            // Get updated storage info if storage settings were changed
            $storageInfo = null;
            if (array_intersect_key($updatedSettings, array_flip(['storage_max_total_size', 'storage_cleanup_enabled']))) {
                $storageInfo = $this->getStorageUsageInfo();
            }

            $response = [
                'message' => 'Configuration updated successfully',
                'updated_settings' => $updatedSettings,
                'updated_count' => count($updatedSettings)
            ];

            if ($storageInfo) {
                $response['storage_info'] = $storageInfo;
            }

            return $this->success($response);

        } catch (Exception $e) {
            return $this->error('Failed to update configuration: '.$e->getMessage());
        }
    }

    /**
     * Get storage usage information and enforce limits
     * @return array Storage usage data
     */
    private function getStorageUsageInfo()
    {
        try {
            $albumsPath = $this->albumsPath;
            $maxSize = $this->dataManager->getConfig('storage_max_total_size') ?? 1073741824; // 1GB default

            // Calculate current storage usage
            $currentUsage = 0;
            $fileCount = 0;
            $albumCount = 0;

            if (is_dir($albumsPath)) {
                $dirs = scandir($albumsPath);
                if ($dirs !== false) {
                    foreach ($dirs as $dir) {
                        if ($dir === '.' || $dir === '..') {
                            continue;
                        }

                        $albumPath = $albumsPath.$dir;
                        if (is_dir($albumPath)) {
                            $albumCount++;
                            $files = scandir($albumPath);
                            if ($files !== false) {
                                foreach ($files as $file) {
                                    if ($file === '.' || $file === '..') {
                                        continue;
                                    }

                                    $filePath = $albumPath.'/'.$file;
                                    if (is_file($filePath)) {
                                        $currentUsage += filesize($filePath);
                                        $fileCount++;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            $usagePercentage = $maxSize > 0 ? round(($currentUsage / $maxSize) * 100, 2) : 0;
            $availableSpace = max(0, $maxSize - $currentUsage);

            return [
                'current_usage' => $currentUsage,
                'current_usage_formatted' => $this->formatBytes($currentUsage),
                'max_size' => $maxSize,
                'max_size_formatted' => $this->formatBytes($maxSize),
                'available_space' => $availableSpace,
                'available_space_formatted' => $this->formatBytes($availableSpace),
                'usage_percentage' => $usagePercentage,
                'file_count' => $fileCount,
                'album_count' => $albumCount,
                'is_near_limit' => $usagePercentage >= 80,
                'is_over_limit' => $usagePercentage >= 100,
                'can_upload' => $usagePercentage < 95 // Leave 5% buffer
            ];

        } catch (Exception $e) {
            error_log('Storage usage calculation error: '.$e->getMessage());
            return [
                'current_usage' => 0,
                'current_usage_formatted' => '0 B',
                'max_size' => $maxSize ?? 1073741824,
                'max_size_formatted' => $this->formatBytes($maxSize ?? 1073741824),
                'available_space' => $maxSize ?? 1073741824,
                'available_space_formatted' => $this->formatBytes($maxSize ?? 1073741824),
                'usage_percentage' => 0,
                'file_count' => 0,
                'album_count' => 0,
                'is_near_limit' => false,
                'is_over_limit' => false,
                'can_upload' => true,
                'error' => 'Failed to calculate storage usage'
            ];
        }
    }

    /**
     * Get storage statistics and usage information
     * @return mixed JSON response with storage statistics
     */
    private function getStorageStats()
    {
        try {
            $stats = $this->dataManager->getStorageStats($this->albumsPath);

            // Format file sizes for display
            $stats['total_size_formatted'] = $this->formatBytes($stats['total_size']);
            $stats['max_storage_size_formatted'] = $this->formatBytes($stats['max_storage_size']);
            $stats['available_space_formatted'] = $this->formatBytes($stats['available_space']);

            // Format largest files
            foreach ($stats['largest_files'] as &$file) {
                $file['size_formatted'] = $this->formatBytes($file['size']);
            }

            // Format album sizes
            foreach ($stats['album_sizes'] as &$album) {
                $album['size_formatted'] = $this->formatBytes($album['size']);
            }

            // Format orphaned files
            foreach ($stats['orphaned_files'] as &$file) {
                $file['size_formatted'] = $this->formatBytes($file['size']);
            }

            // Format file types
            foreach ($stats['file_types'] as $ext => &$typeInfo) {
                $typeInfo['size_formatted'] = $this->formatBytes($typeInfo['size']);
            }

            return $this->success([
                'message' => 'Storage statistics retrieved successfully',
                'stats' => $stats
            ]);

        } catch (Exception $e) {
            return $this->error('Failed to get storage statistics: '.$e->getMessage());
        }
    }

    /**
     * Clean up orphaned files (files not referenced in database)
     * @return mixed JSON response with cleanup results
     */
    private function cleanupOrphanedFiles()
    {
        try {
            // Get JSON input
            $input = json_decode(file_get_contents('php://input'), true);
            $dryRun = isset($input['dry_run']) ? (bool) $input['dry_run'] : false;

            $result = $this->dataManager->cleanupOrphanedFiles($this->albumsPath, $dryRun);

            // Format file sizes for display
            $result['space_freed_formatted'] = $this->formatBytes($result['space_freed']);

            foreach ($result['deleted_files'] as &$file) {
                $file['size_formatted'] = $this->formatBytes($file['size']);
            }

            foreach ($result['deleted_thumbnails'] as &$file) {
                $file['size_formatted'] = $this->formatBytes($file['size']);
            }

            $message = $dryRun
            ? 'Dry run completed - no files were actually deleted'
            : 'Orphaned files cleanup completed successfully';

            return $this->success([
                'message' => $message,
                'dry_run' => $dryRun,
                'cleanup_result' => $result
            ]);

        } catch (Exception $e) {
            return $this->error('Failed to cleanup orphaned files: '.$e->getMessage());
        }
    }

    /**
     * Generate missing thumbnails for photos
     * @return mixed JSON response with thumbnail generation results
     */
    private function generateThumbnails()
    {
        try {
            // Get configuration for image processing
            $config = $this->dataManager->getAllConfig();
            $imageProcessor = new ImageProcessor($config);

            $result = $this->dataManager->generateMissingThumbnails($this->albumsPath, $imageProcessor);

            // Format file sizes for display
            foreach ($result['generated_thumbnails'] as &$thumbnail) {
                $thumbnail['size_formatted'] = $this->formatBytes($thumbnail['size']);
            }

            return $this->success([
                'message' => 'Thumbnail generation completed successfully',
                'generation_result' => $result
            ]);

        } catch (Exception $e) {
            return $this->error('Failed to generate thumbnails: '.$e->getMessage());
        }
    }

    /**
     * Get storage usage by album
     * @return mixed JSON response with album storage usage
     */
    private function getAlbumStorageUsage()
    {
        try {
            $usage = $this->dataManager->getAlbumStorageUsage($this->albumsPath);

            return $this->success([
                'message' => 'Album storage usage retrieved successfully',
                'album_usage' => $usage
            ]);

        } catch (Exception $e) {
            return $this->error('Failed to get album storage usage: '.$e->getMessage());
        }
    }

    /**
     * Send JSON response with proper HTTP status code
     * @param array $response Response data
     * @return string JSON response
     */
    private function sendResponse($response)
    {
        // Set HTTP status code if specified
        if (isset($response['error']['http_status'])) {
            http_response_code($response['error']['http_status']);
        } elseif (isset($response['meta']['http_status'])) {
            http_response_code($response['meta']['http_status']);
        }

        return json_encode($response, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    }

    /**
     * Trigger RSS feed auto-update mechanism
     * Called when photos are added or albums are modified
     * @return bool Success status
     */
    private function triggerRSSAutoUpdate()
    {
        try {
            // Get base URL from configuration
            $config = $this->dataManager->getAllConfig();
            $baseUrl = $config['rss_base_url'] ?? $this->getBaseUrl();

            // Create RSS generator
            $rssGenerator = new RSSGenerator($this->dataManager, $baseUrl);

            // Trigger auto-update
            return $rssGenerator->triggerAutoUpdate();

        } catch (Exception $e) {
            $this->errorHandler->logError(
                'RSS auto-update failed: '.$e->getMessage(),
                'RSS_AUTO_UPDATE_ERROR',
                ['exception' => get_class($e)],
                ['method' => 'triggerRSSAutoUpdate']
            );
            return false;
        }
    }

    /**
     * Get base URL from current request
     * @return string Base URL
     */
    private function getBaseUrl()
    {
        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
        $path = dirname($_SERVER['SCRIPT_NAME']);

        return $protocol.$host.rtrim($path, '/').'/';
    }

    /**
     * Check authentication status
     * @return string JSON response
     */
    private function checkAuthStatus()
    {
        $userInfo = $this->auth->getCurrentUser();
        $successResponse = $this->errorHandler->createSuccessResponse($userInfo);
        return $this->sendResponse($successResponse);
    }
}

// Handle the request
$api = new PhotoGalleryAPI();
echo $api->handleRequest();