safeLoad(); } if (!isset($_SESSION['user_id'])) { http_response_code(401); echo json_encode(['error' => 'Benutzer nicht authentifiziert.']); exit; } $currentUserId = $_SESSION['user_id']; // === HELPER FUNCTIONS === function callGeminiAPI(array $payload, string $apiKey): array { $googleApiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'; $fullApiUrl = $googleApiUrl . '?key=' . $apiKey; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $fullApiUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 90, ]); $response_body = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch) || $http_code >= 400) { $error_message = curl_errno($ch) ? curl_error($ch) : 'KI-API-Fehler mit Status ' . $http_code . ' - ' . $response_body; curl_close($ch); throw new Exception('Fehler bei der Kommunikation mit der KI: ' . $error_message); } curl_close($ch); return json_decode($response_body, true); } function processUploadedPhoto(array $fileInfo, int $targetWidth = 800): ?string { if ($fileInfo['error'] !== UPLOAD_ERR_OK) return null; $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!in_array(mime_content_type($fileInfo['tmp_name']), $allowedMimeTypes) || $fileInfo['size'] > (10 * 1024 * 1024)) return null; $uploadDir = 'uploads/'; if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); $fileName = uniqid() . '-' . pathinfo($fileInfo['name'], PATHINFO_FILENAME) . '.webp'; $targetFilePath = $uploadDir . $fileName; $sourceImage = null; switch (exif_imagetype($fileInfo['tmp_name'])) { case IMAGETYPE_JPEG: $sourceImage = imagecreatefromjpeg($fileInfo['tmp_name']); break; case IMAGETYPE_PNG: $sourceImage = imagecreatefrompng($fileInfo['tmp_name']); break; case IMAGETYPE_WEBP: $sourceImage = imagecreatefromwebp($fileInfo['tmp_name']); break; } if (!$sourceImage) return null; $resizedImage = (imagesx($sourceImage) > $targetWidth) ? imagescale($sourceImage, $targetWidth) : $sourceImage; imagewebp($resizedImage, $targetFilePath, 85); imagedestroy($sourceImage); if ($resizedImage !== $sourceImage) imagedestroy($resizedImage); return $targetFilePath; } function getPlantWithDueDates($pdo, $plantId, $userId) { $stmt = $pdo->prepare("SELECT p.id, p.name, p.latin_name, p.photo, p.last_watered, p.last_fertilized, p.watering_interval_days, p.fertilizing_interval_days, p.care_tips FROM plants p WHERE p.id = ? AND p.user_id = ?"); $stmt->execute([$plantId, $userId]); $plant = $stmt->fetch(PDO::FETCH_ASSOC); if ($plant) { $plant['watering_interval'] = $plant['watering_interval_days']; $plant['fertilizing_interval'] = $plant['fertilizing_interval_days']; $isValidDate = fn($dateStr) => !empty($dateStr) && $dateStr !== '0000-00-00'; if ((int)$plant['watering_interval'] > 0 && $isValidDate($plant['last_watered'])) { $plant['next_watering_due'] = (new DateTime($plant['last_watered']))->add(new DateInterval('P' . $plant['watering_interval'] . 'D'))->format('Y-m-d'); } else { $plant['next_watering_due'] = null; } if ((int)$plant['fertilizing_interval'] > 0 && $isValidDate($plant['last_fertilized'])) { $plant['next_fertilizing_due'] = (new DateTime($plant['last_fertilized']))->add(new DateInterval('P' . $plant['fertilizing_interval'] . 'D'))->format('Y-m-d'); } else { $plant['next_fertilizing_due'] = null; } } return $plant; } // === CREDIT MANAGEMENT === try { $stmt = $pdo->prepare("SELECT ai_credits_last_reset FROM users WHERE id = ?"); $stmt->execute([$currentUserId]); $lastReset = $stmt->fetchColumn(); $now = new DateTime(); if ($lastReset === null || (new DateTime($lastReset))->format('Y-m') < $now->format('Y-m')) { $stmt = $pdo->prepare("UPDATE users SET ai_credits = 100, ai_credits_last_reset = NOW() WHERE id = ?"); $stmt->execute([$currentUserId]); } } catch (Exception $e) { /* non-blocking */ } // === API ROUTING === $method = $_SERVER['REQUEST_METHOD']; $json_data = json_decode(file_get_contents('php://input'), true); header('Content-Type: application/json'); try { // --- Handle JSON-based POST requests FIRST --- if ($method === 'POST' && !empty($json_data)) { $request_type = $json_data['request_type'] ?? ''; switch ($request_type) { case 'ai_identification': case 'ai_enrichment': $pdo->beginTransaction(); $stmt = $pdo->prepare("UPDATE users SET ai_credits = ai_credits - 1 WHERE id = ? AND ai_credits >= 1"); $stmt->execute([$currentUserId]); $geminiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?: ''; if (empty($geminiKey) && file_exists(__DIR__ . '/../home/.env')) { foreach (file(__DIR__ . '/../home/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') continue; if (strpos($line, '=') !== false) { list($k, $v) = explode('=', $line, 2); if (trim($k) === 'GEMINI_API_KEY') { $geminiKey = trim(trim($v), '"\''); break; } } } } $response_data = callGeminiAPI($json_data['payload'], $geminiKey); $pdo->commit(); echo json_encode($response_data); break; case 'update_user_settings': $settings = $json_data['settings'] ?? []; $allowedKeys = ['theme', 'allow_photo_sharing', 'auto_fill_watering', 'auto_fill_fertilizing', 'auto_fill_care_tips']; $sqlParts = []; $params = []; foreach ($settings as $key => $value) { if (in_array($key, $allowedKeys)) { $sqlParts[] = "$key = ?"; $params[] = is_bool($value) ? (int)$value : $value; } } if (empty($sqlParts)) throw new Exception('Keine gültigen Einstellungsfelder.', 400); $params[] = $currentUserId; $stmt = $pdo->prepare("UPDATE users SET " . implode(', ', $sqlParts) . " WHERE id = ?"); $stmt->execute($params); echo json_encode(['success' => true]); break; case 'care_action': $plantId = $json_data['id']; $column = ($json_data['action'] === 'water') ? 'last_watered' : 'last_fertilized'; $entryType = ($json_data['action'] === 'water') ? 'watered' : 'fertilized'; $pdo->beginTransaction(); $stmt = $pdo->prepare("UPDATE plants SET $column = CURDATE() WHERE id = ? AND user_id = ?"); $stmt->execute([$plantId, $currentUserId]); $stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, entry_type) VALUES (?, ?, ?)"); $stmt->execute([$plantId, $currentUserId, $entryType]); $pdo->commit(); echo json_encode(['success' => true, 'plant' => getPlantWithDueDates($pdo, $plantId, $currentUserId)]); break; case 'set_profile_picture': $stmt = $pdo->prepare("UPDATE users SET profile_image = ? WHERE id = ?"); $stmt->execute([$json_data['photo_path'], $currentUserId]); echo json_encode(['success' => true, 'new_image' => $json_data['photo_path']]); break; default: throw new Exception('Unbekannter JSON-Request-Typ.', 400); } exit; } // --- Handle GET, DELETE, and multipart/form-data POST requests --- switch ($method) { case 'GET': $action = $_GET['action'] ?? 'get_plants'; switch ($action) { case 'get_plants': $stmt = $pdo->prepare("SELECT id FROM plants WHERE user_id = ? ORDER BY id DESC"); $stmt->execute([$currentUserId]); $plant_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); echo json_encode(array_map(fn($id) => getPlantWithDueDates($pdo, $id, $currentUserId), $plant_ids)); break; case 'get_diary': $stmt = $pdo->prepare("SELECT * FROM plant_diary WHERE plant_id = ? AND user_id = ? ORDER BY entry_date DESC"); $stmt->execute([$_GET['plant_id'], $currentUserId]); echo json_encode($stmt->fetchAll()); break; case 'get_user_settings': $stmt = $pdo->prepare("SELECT username, email, profile_image, theme, allow_photo_sharing, ai_credits, auto_fill_watering, auto_fill_fertilizing, auto_fill_care_tips FROM users WHERE id = ?"); $stmt->execute([$currentUserId]); $userSettings = $stmt->fetch(); $stmt_count = $pdo->prepare("SELECT COUNT(id) as plant_count FROM plants WHERE user_id = ?"); $stmt_count->execute([$currentUserId]); $userSettings['plant_count'] = $stmt_count->fetchColumn(); echo json_encode($userSettings); break; case 'get_lexicon_entries': $stmt = $pdo->prepare("SELECT pl.name, pl.latin_name, lp.photo_path as photo_url FROM plant_lexicon pl LEFT JOIN (SELECT plant_latin_name, photo_path, ROW_NUMBER() OVER(PARTITION BY plant_latin_name ORDER BY ai_photo_quality_score DESC, id DESC) as rn FROM lexicon_photos WHERE status = 'approved') lp ON pl.latin_name = lp.plant_latin_name AND lp.rn = 1 WHERE pl.entry_count > 0 ORDER BY pl.name ASC"); $stmt->execute(); echo json_encode($stmt->fetchAll()); break; case 'get_lexicon_entry_details': $latinName = $_GET['latin_name'] ?? ''; if (empty($latinName)) throw new Exception('Kein botanischer Name angegeben.', 400); $stmt = $pdo->prepare("SELECT * FROM plant_lexicon WHERE latin_name = ?"); $stmt->execute([$latinName]); $plantDetails = $stmt->fetch(); if (!$plantDetails) throw new Exception('Pflanze nicht im Lexikon gefunden.', 404); $stmtPhotos = $pdo->prepare("SELECT photo_path FROM lexicon_photos WHERE plant_latin_name = ? AND status = 'approved' ORDER BY ai_photo_quality_score DESC, id DESC LIMIT 10"); $stmtPhotos->execute([$latinName]); $plantDetails['photo_gallery'] = $stmtPhotos->fetchAll(PDO::FETCH_COLUMN); echo json_encode($plantDetails); break; default: throw new Exception('Ungültige GET-Aktion.', 400); } break; case 'POST': $action = $_POST['action'] ?? 'save_plant'; switch ($action) { case 'add_diary_entry': $photoPath = isset($_FILES['photo']) ? processUploadedPhoto($_FILES['photo'], 600) : null; $stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, notes, photo, entry_type) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$_POST['plant_id'], $currentUserId, $_POST['notes'] ?? null, $photoPath, isset($_POST['is_milestone']) ? 'milestone' : 'manual']); $newEntryId = $pdo->lastInsertId(); $stmt = $pdo->prepare("SELECT * FROM plant_diary WHERE id = ?"); $stmt->execute([$newEntryId]); echo json_encode(['success' => true, 'entry' => $stmt->fetch()]); break; case 'save_plant': $name = trim($_POST['name'] ?? ''); if (empty($name)) throw new Exception('Der Name der Pflanze darf nicht leer sein.', 400); $plantId = $_POST['id'] ?? null; $latinName = trim($_POST['latin_name'] ?? '') ?: null; $photoPath = isset($_FILES['photo']) ? processUploadedPhoto($_FILES['photo']) : null; $finalPhotoPath = $photoPath ?? $_POST['current_photo'] ?? null; // ROBUSTE PRÜFUNG DER INTERVALLE $wateringInterval = (isset($_POST['watering_interval']) && is_numeric($_POST['watering_interval'])) ? (int)$_POST['watering_interval'] : null; $fertilizingInterval = (isset($_POST['fertilizing_interval']) && is_numeric($_POST['fertilizing_interval'])) ? (int)$_POST['fertilizing_interval'] : null; $careTips = trim($_POST['care_tips'] ?? '') ?: null; $pdo->beginTransaction(); $oldLatinName = null; if ($plantId) { // Edit $stmtOld = $pdo->prepare("SELECT latin_name FROM plants WHERE id = ? AND user_id = ?"); $stmtOld->execute([$plantId, $currentUserId]); $oldLatinName = $stmtOld->fetchColumn(); $stmt = $pdo->prepare("UPDATE plants SET name = ?, latin_name = ?, photo = ?, watering_interval_days = ?, fertilizing_interval_days = ?, care_tips = ? WHERE id = ? AND user_id = ?"); $stmt->execute([$name, $latinName, $finalPhotoPath, $wateringInterval, $fertilizingInterval, $careTips, $plantId, $currentUserId]); } else { // Add $stmt = $pdo->prepare("INSERT INTO plants (user_id, name, latin_name, photo, last_watered, last_fertilized, watering_interval_days, fertilizing_interval_days, care_tips) VALUES (?, ?, ?, ?, CURDATE(), CURDATE(), ?, ?, ?)"); $stmt->execute([$currentUserId, $name, $latinName, $finalPhotoPath, $wateringInterval, $fertilizingInterval, $careTips]); $plantId = $pdo->lastInsertId(); $stmt = $pdo->prepare("INSERT INTO plant_diary (plant_id, user_id, entry_type, notes) VALUES (?, ?, 'milestone', 'Pflanze hinzugefügt')"); $stmt->execute([$plantId, $currentUserId]); } // Handle lexicon entry count changes if ($oldLatinName && $oldLatinName !== $latinName) { $stmtDec = $pdo->prepare("UPDATE plant_lexicon SET entry_count = entry_count - 1 WHERE latin_name = ? AND entry_count > 0"); $stmtDec->execute([$oldLatinName]); } if ($latinName && (!$oldLatinName || $oldLatinName !== $latinName)) { $stmt = $pdo->prepare("INSERT INTO plant_lexicon (latin_name, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE entry_count = entry_count + 1, name = VALUES(name)"); $stmt->execute([$latinName, $name]); } // Process AI data for lexicon if available $enrichmentData = json_decode($_POST['ai_enrichment_json'] ?? '{}', true); if(!empty($enrichmentData)) { $stmt = $pdo->prepare("UPDATE plant_lexicon SET plant_family = :plant_family, plant_genus = :plant_genus, description_full = :description_full, care_tips = :care_tips, watering_interval_days = :watering_interval_days, fertilizing_interval_days = :fertilizing_interval_days, light_requirement = :light_requirement, water_requirement = :water_requirement, humidity_requirement = :humidity_requirement, care_difficulty = :care_difficulty, growth_habit = :growth_habit, mature_size = :mature_size, toxicity = :toxicity, edibility = :edibility, temperature_preference = :temperature_preference, flowering_period = :flowering_period, substrate_recommendation = :substrate_recommendation, native_to = :native_to, tags = :tags, updated_at = NOW() WHERE latin_name = :latin_name AND description_full IS NULL"); $stmt->execute([ 'plant_family' => $enrichmentData['plant_family'] ?? null, 'plant_genus' => $enrichmentData['plant_genus'] ?? null, 'description_full' => $enrichmentData['description_full'] ?? null, 'care_tips' => $enrichmentData['care_tips'] ?? null, 'watering_interval_days' => $enrichmentData['watering_interval_days'] ?? null, 'fertilizing_interval_days' => $enrichmentData['fertilizing_interval_days'] ?? null, 'light_requirement' => $enrichmentData['light_requirement'] ?? null, 'water_requirement' => $enrichmentData['water_requirement'] ?? null, 'humidity_requirement' => $enrichmentData['humidity_requirement'] ?? null, 'care_difficulty' => $enrichmentData['care_difficulty'] ?? null, 'growth_habit' => $enrichmentData['growth_habit'] ?? null, 'mature_size' => $enrichmentData['mature_size'] ?? null, 'toxicity' => $enrichmentData['toxicity'] ?? null, 'edibility' => $enrichmentData['edibility'] ?? null, 'temperature_preference' => $enrichmentData['temperature_preference'] ?? null, 'flowering_period' => $enrichmentData['flowering_period'] ?? null, 'substrate_recommendation' => $enrichmentData['substrate_recommendation'] ?? null, 'native_to' => $enrichmentData['native_to'] ?? null, 'tags' => $enrichmentData['tags'] ?? null, 'latin_name' => $latinName ]); } // Process photo for lexicon if allowed and available $fullAiResponse = json_decode($_POST['ai_full_response_json'] ?? '{}', true); $stmtAllow = $pdo->prepare("SELECT allow_photo_sharing FROM users WHERE id = ?"); $stmtAllow->execute([$currentUserId]); if ($stmtAllow->fetchColumn() && $finalPhotoPath && $photoPath && !empty($fullAiResponse['photo_analysis'])) { $photoData = $fullAiResponse['photo_analysis']; $isHighQuality = $photoData['is_high_quality'] ?? false; $stmt = $pdo->prepare("INSERT INTO lexicon_photos (user_id, plant_latin_name, photo_path, status, rejection_reason, ai_confidence, photo_type, focal_point_x, focal_point_y, dominant_color_hex, ai_photo_quality_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([ $currentUserId, $latinName, $finalPhotoPath, ($isHighQuality ? 'approved' : 'rejected'), $photoData['rejection_reason'] ?? null, $fullAiResponse['identification']['confidence_score'] ?? null, $photoData['photo_type'] ?? null, $photoData['focal_point']['x'] ?? null, $photoData['focal_point']['y'] ?? null, $photoData['dominant_color_hex'] ?? null, $photoData['photo_quality_score'] ?? null ]); } $pdo->commit(); echo json_encode(['success' => true, 'plant' => getPlantWithDueDates($pdo, $plantId, $currentUserId)]); break; default: throw new Exception('Ungültige Formular-Aktion.', 400); } break; case 'DELETE': $plantId = $_GET['id'] ?? null; if ($plantId) { $pdo->beginTransaction(); $stmtLatin = $pdo->prepare("SELECT latin_name FROM plants WHERE id = ? AND user_id = ?"); $stmtLatin->execute([$plantId, $currentUserId]); $latinNameToDecrement = $stmtLatin->fetchColumn(); $stmt = $pdo->prepare("DELETE FROM plants WHERE id = ? AND user_id = ?"); $stmt->execute([$plantId, $currentUserId]); if ($latinNameToDecrement) { $stmtDec = $pdo->prepare("UPDATE plant_lexicon SET entry_count = entry_count - 1 WHERE latin_name = ? AND entry_count > 0"); $stmtDec->execute([$latinNameToDecrement]); } $pdo->commit(); echo json_encode(['success' => true]); } else if (isset($_GET['diary_entry_id'])) { $entryId = $_GET['diary_entry_id']; $stmt = $pdo->prepare("DELETE FROM plant_diary WHERE id = ? AND user_id = ?"); $stmt->execute([$entryId, $currentUserId]); echo json_encode(['success' => $stmt->rowCount() > 0]); } else { throw new Exception('Keine ID zum Löschen angegeben.', 400); } break; default: throw new Exception('Methode nicht erlaubt.', 405); } } catch (Exception $e) { if ($pdo->inTransaction()) $pdo->rollBack(); $code = $e->getCode(); if (!is_int($code) || $code < 400 || $code >= 600) { $code = 500; } http_response_code($code); echo json_encode(['error' => $e->getMessage()]); } ?>