<?php
declare(strict_types=1);

namespace App;

final class Search
{
    public function __construct(private ?\PDO $pdo) {}

    public function searchEvents(string $query, int $limit = 100, ?array $geo = null): array
    {
        if (!$this->pdo) return [];

        $q = trim($query);
        $hasGeo = isset($geo['lat'], $geo['lng']) && is_numeric($geo['lat']) && is_numeric($geo['lng']);
        if ($q === '' && !$hasGeo) return [];

        $tokens = array_filter(preg_split('/\s+/', $q) ?: [], fn($t) => $t !== '');
        if (!$tokens) {
            $tokens = [$q];
        }
        // Nur Tokens ab 3 Zeichen für fuzzy/LIKE berücksichtigen
        $tokens = array_values(array_filter($tokens, fn($t) => mb_strlen($t) >= 3));
        if (!$tokens && !$hasGeo) return [];

        $conditions = [];
        $bindTokens = [];
        $i = 0;
        foreach ($tokens as $tok) {
            $tok = trim($tok);
            if ($tok === '') continue;
            // LIKE + phonetic (SOUNDEX) to allow partial and typo-tolerant matches
            $conditions[] = "(title LIKE CONCAT('%', ?, '%') OR teaser_public LIKE CONCAT('%', ?, '%') OR description LIKE CONCAT('%', ?, '%') OR city LIKE CONCAT('%', ?, '%') OR region LIKE CONCAT('%', ?, '%') OR zip LIKE CONCAT('%', ?, '%') OR SOUNDEX(title)=SOUNDEX(?) OR SOUNDEX(teaser_public)=SOUNDEX(?) OR SOUNDEX(description)=SOUNDEX(?) OR SOUNDEX(city)=SOUNDEX(?) OR SOUNDEX(region)=SOUNDEX(?))";
            // LIKE bindings
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            // SOUNDEX bindings
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $bindTokens[] = $tok;
            $i++;
        }

        $whereParts = [
            "starts_at >= NOW()",
            "status != 'cancelled'",
        ];
        if ($conditions) {
            // "OR" so that partial matches across tokens are allowed
            $whereParts[] = '(' . implode(' OR ', $conditions) . ')';
        }

        $distanceFiltering = false;
        $bind = [];
        if ($hasGeo) {
            $sql = "SELECT id, title, teaser_public, description, city, region, zip, starts_at, visibility, allow_kids, location_label, lat, lng,
            (6371 * ACOS(LEAST(1,
              COS(RADIANS(?)) * COS(RADIANS(lat)) * COS(RADIANS(lng) - RADIANS(?)) +
              SIN(RADIANS(?)) * SIN(RADIANS(lat))
            ))) AS distance_km";
            $lat = (float)$geo['lat'];
            $lng = (float)$geo['lng'];
            $radius = isset($geo['radius']) && is_numeric($geo['radius']) ? max(0.1, (float)$geo['radius']) : 5.0;
            $distanceFiltering = true;

            $latRange = $radius / 111.0;
            $lngRange = $radius / (111.0 * max(0.1, cos($lat * M_PI / 180)));
            $whereParts[] = "(lat IS NOT NULL AND lng IS NOT NULL)";
            $whereParts[] = "(lat BETWEEN ? AND ?)";
            $whereParts[] = "(lng BETWEEN ? AND ?)";
            // Haversine params (order must match SQL): first three
            $bind[] = $lat; // COS(RADIANS(?))
            $bind[] = $lng; // COS(RADIANS(lng) - RADIANS(?))
            $bind[] = $lat; // SIN(RADIANS(?))
            // THEN token binds
            $bind = array_merge($bind, $bindTokens);
            // Bounding box
            $bind[] = $lat - $latRange;
            $bind[] = $lat + $latRange;
            $bind[] = $lng - $lngRange;
            $bind[] = $lng + $lngRange;
            // Radius for HAVING
            $bind[] = $radius;
        } else {
            $sql = "SELECT id, title, teaser_public, description, city, region, zip, starts_at, visibility, allow_kids, location_label, lat, lng, NULL AS distance_km";
            $bind = $bindTokens;
        }

        $where = $whereParts ? ('WHERE ' . implode(' AND ', $whereParts)) : '';
        $sql .= " FROM events $where";
        if ($distanceFiltering) {
            $sql .= " HAVING distance_km <= ?";
            $sql .= " ORDER BY distance_km ASC, starts_at ASC";
        } else {
            $sql .= " ORDER BY starts_at ASC";
        }
        $limit = (int)$limit;
        $sql .= " LIMIT {$limit}";

        $stmt = $this->pdo->prepare($sql);
        try {
            $stmt->execute($bind);
            $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
            if (!$hasGeo) {
                foreach ($rows as &$r) {
                    unset($r['distance_km']);
                }
                unset($r);
            }
            // Fuzzy filter: allow slight typos (Levenshtein <= 1 or 2)
            if ($tokens) {
                $rows = array_values(array_filter($rows, function ($row) use ($tokens) {
                    $haystack = strtolower(
                        ($row['title'] ?? '') . ' ' .
                        ($row['teaser_public'] ?? '') . ' ' .
                        ($row['description'] ?? '') . ' ' .
                        ($row['city'] ?? '') . ' ' .
                        ($row['region'] ?? '')
                    );
                    $words = preg_split('/[^a-z0-9äöüß]+/i', $haystack) ?: [];
                    foreach ($tokens as $tok) {
                        $t = strtolower($tok);
                        if ($t === '') continue;
                        if (str_contains($haystack, $t)) {
                            return true;
                        }
                        foreach ($words as $w) {
                            if ($w === '') continue;
                            $dist = levenshtein($t, $w);
                            if ($dist <= 1 || ($dist <= 2 && max(strlen($t), strlen($w)) > 4)) {
                                return true;
                            }
                        }
                    }
                    return false;
                }));
            }
            // Fallback: wenn keine Treffer, erneut ohne Token-Filter laden und nur fuzzy filtern
            if (!$rows && $tokens) {
                $wherePartsFallback = [
                    "starts_at >= NOW()",
                    "status != 'cancelled'",
                ];
                $bindFb = [];
                $sqlFb = '';
                if ($hasGeo) {
                    $sqlFb = "SELECT id, title, teaser_public, description, city, region, zip, starts_at, visibility, allow_kids, location_label, lat, lng,
                    (6371 * ACOS(LEAST(1,
                      COS(RADIANS(?)) * COS(RADIANS(lat)) * COS(RADIANS(lng) - RADIANS(?)) +
                      SIN(RADIANS(?)) * SIN(RADIANS(lat))
                    ))) AS distance_km";
                    $lat = (float)$geo['lat'];
                    $lng = (float)$geo['lng'];
                    $radius = isset($geo['radius']) && is_numeric($geo['radius']) ? max(0.1, (float)$geo['radius']) : 5.0;
                    $latRange = $radius / 111.0;
                    $lngRange = $radius / (111.0 * max(0.1, cos($lat * M_PI / 180)));
                    $wherePartsFallback[] = "(lat IS NOT NULL AND lng IS NOT NULL)";
                    $wherePartsFallback[] = "(lat BETWEEN ? AND ?)";
                    $wherePartsFallback[] = "(lng BETWEEN ? AND ?)";
                    $bindFb[] = $lat;
                    $bindFb[] = $lng;
                    $bindFb[] = $lat;
                    $bindFb[] = $lat - $latRange;
                    $bindFb[] = $lat + $latRange;
                    $bindFb[] = $lng - $lngRange;
                    $bindFb[] = $lng + $lngRange;
                    $bindFb[] = $radius;
                    $havingFb = true;
                } else {
                    $sqlFb = "SELECT id, title, teaser_public, description, city, region, zip, starts_at, visibility, allow_kids, location_label, lat, lng, 1 AS distance_km";
                    $havingFb = false;
                }
                $whereFb = $wherePartsFallback ? ('WHERE ' . implode(' AND ', $wherePartsFallback)) : '';
                $sqlFb .= " FROM events $whereFb";
                if ($havingFb) {
                    $sqlFb .= " HAVING distance_km <= ?";
                    $sqlFb .= " ORDER BY distance_km ASC, starts_at ASC";
                } else {
                    $sqlFb .= " ORDER BY starts_at ASC";
                }
                $sqlFb .= " LIMIT {$limit}";
                $stmtFb = $this->pdo->prepare($sqlFb);
                $stmtFb->execute($bindFb);
                $rowsFb = $stmtFb->fetchAll(\PDO::FETCH_ASSOC) ?: [];
                if ($rowsFb) {
                    $rows = array_values(array_filter($rowsFb, function ($row) use ($tokens) {
                        $haystack = strtolower(
                            ($row['title'] ?? '') . ' ' .
                            ($row['teaser_public'] ?? '') . ' ' .
                            ($row['description'] ?? '') . ' ' .
                            ($row['city'] ?? '') . ' ' .
                            ($row['region'] ?? '')
                        );
                        $words = preg_split('/[^a-z0-9äöüß]+/i', $haystack) ?: [];
                        foreach ($tokens as $tok) {
                            $t = strtolower($tok);
                            if ($t === '') continue;
                            if (str_contains($haystack, $t)) {
                                return true;
                            }
                            foreach ($words as $w) {
                                if ($w === '') continue;
                                $dist = levenshtein($t, $w);
                                if ($dist <= 1 || ($dist <= 2 && max(strlen($t), strlen($w)) > 4)) {
                                    return true;
                                }
                            }
                        }
                        return false;
                    }));
                }
            }
            if (defined('APP_ENV') && APP_ENV === 'staging') {
                $logOk = [
                    'status' => 'ok',
                    'sql' => $sql,
                    'bind' => $bind,
                    'count' => count($rows),
                    'fallback' => ($rows ? 'primary' : 'fallback'),
                ];
                @file_put_contents(__DIR__ . '/../../debug/search_debug.log', print_r($logOk, true));
            }
            return $rows;
        } catch (\PDOException $e) {
            // Log into /debug/search_debug.log and continue with empty results
            $logErr = [
                'error' => $e->getMessage(),
                'sql' => $sql,
                'bind' => $bind,
            ];
            @file_put_contents(__DIR__ . '/../../debug/search_debug.log', print_r($logErr, true));
            return [];
        }
    }
}
