<?php

namespace App\Services\FishScore;

use App\Models\Fish\Species;
use Carbon\Carbon;

/**
 * Generic balık skor motoru.
 * Her tür sadece kendi davranışıyla ilgili özellikleri deklare eder.
 * Yeni tür eklemek bir veri operasyonu, kod operasyonu değil.
 */
class FishScoreEngine
{
    public function __construct(
        private FeatureCalculator $featureCalculator,
    ) {}

    /**
     * Ana hesaplama: (lat, lon, species_id, time) → FishScoreResult
     */
    public function calculateScore(float $lat, float $lon, int $speciesId, Carbon $time, ?string $region = null): FishScoreResult
    {
        $species = Species::with(['featurePrefs', 'preyRelations', 'regionalOverrides'])->findOrFail($speciesId);

        $staticFeatures = $this->featureCalculator->getStaticFeatures($lat, $lon);
        $dynamicFeatures = $this->featureCalculator->getDynamicFeatures($lat, $lon, $time);
        $allFeatures = array_merge($staticFeatures, $dynamicFeatures);

        $prefs = $species->getEffectivePrefs($region);

        $staticSubScores = [];
        $dynamicSubScores = [];
        $allSubScores = [];

        foreach ($prefs as $key => $pref) {
            $prefData = is_array($pref) ? $pref : $pref->toArray();
            $featureKey = $prefData['feature_key'] ?? $key;
            $evalType = $prefData['eval_type'];
            $params = $prefData['params'] ?? [];
            $weight = (float) ($prefData['weight'] ?? 0.5);

            if (! isset($allFeatures[$featureKey])) {
                continue;
            }

            $value = $allFeatures[$featureKey];

            // Bilinmeyen / null değerli özellikleri atla — ceza vermesin
            if ($value === null || $value === 'unknown') {
                continue;
            }

            $subScore = $this->evaluate($evalType, $value, $params);

            $scoreEntry = [
                'feature_key' => $featureKey,
                'eval_type' => $evalType,
                'value' => $value,
                'sub_score' => $subScore,
                'weight' => $weight,
                'weighted_contribution' => $subScore * $weight,
            ];

            $allSubScores[] = $scoreEntry;

            if ($this->isStaticFeature($featureKey)) {
                $staticSubScores[] = $scoreEntry;
            } else {
                $dynamicSubScores[] = $scoreEntry;
            }
        }

        $habitatScore = $this->weightedAverage($staticSubScores);

        $dynamicRaw = $this->weightedAverage($dynamicSubScores);
        $dynamicModifier = ! empty($dynamicSubScores)
            ? max(0.4, min(1.6, ($dynamicRaw / 100) * 1.6))
            : 1.0;

        $preyMultiplier = $this->calculatePreyMultiplier($species, $lat, $lon);

        $rawFinal = $habitatScore * $dynamicModifier * $preyMultiplier;
        $finalScore = (int) round(max(0, min(100, $rawFinal)));

        $explanations = $this->generateExplanations($allSubScores);

        return new FishScoreResult(
            finalScore: $finalScore,
            habitatScore: (int) round($habitatScore),
            dynamicModifier: $dynamicModifier,
            preyMultiplier: $preyMultiplier,
            explanations: $explanations,
            subScores: $allSubScores,
            features: $allFeatures,
        );
    }

    /**
     * Sadece habitat (statik) bileşeni — static_habitat_scores ön hesap için.
     */
    public function calculateHabitatScoreOnly(float $lat, float $lon, int $speciesId, ?string $region = null): int
    {
        $species = Species::with(['featurePrefs', 'regionalOverrides'])->findOrFail($speciesId);

        $staticFeatures = $this->featureCalculator->getStaticFeatures($lat, $lon);
        $prefs = $species->getEffectivePrefs($region);

        $staticSubScores = [];
        foreach ($prefs as $key => $pref) {
            $prefData = is_array($pref) ? $pref : $pref->toArray();
            $featureKey = $prefData['feature_key'] ?? $key;
            $evalType = $prefData['eval_type'];
            $params = $prefData['params'] ?? [];
            $weight = (float) ($prefData['weight'] ?? 0.5);

            if (! isset($staticFeatures[$featureKey])) {
                continue;
            }

            if (! $this->isStaticFeature($featureKey)) {
                continue;
            }

            $value = $staticFeatures[$featureKey];
            if ($value === null || $value === 'unknown') {
                continue;
            }

            $subScore = $this->evaluate($evalType, $value, $params);

            $staticSubScores[] = [
                'feature_key' => $featureKey,
                'eval_type' => $evalType,
                'value' => $value,
                'sub_score' => $subScore,
                'weight' => $weight,
                'weighted_contribution' => $subScore * $weight,
            ];
        }

        return (int) round($this->weightedAverage($staticSubScores));
    }

    /**
     * Tüm published türler için batch skor.
     */
    public function calculateScoresForPoint(float $lat, float $lon, Carbon $time, ?string $region = null): array
    {
        $species = Species::published()->with(['featurePrefs', 'preyRelations'])->get();
        $results = [];

        foreach ($species as $sp) {
            try {
                $results[] = [
                    'species' => ['id' => $sp->id, 'slug' => $sp->slug, 'tr_name' => $sp->tr_name, 'photo_url' => $sp->photo_url],
                    'score' => $this->calculateScore($lat, $lon, $sp->id, $time, $region)->toArray(),
                ];
            } catch (\Exception $e) {
                continue;
            }
        }

        usort($results, fn ($a, $b) => $b['score']['final_score'] <=> $a['score']['final_score']);

        return $results;
    }

    // ──────────── Değerlendirme Fonksiyonları ────────────

    public function evaluate(string $evalType, mixed $value, array $params): float
    {
        return match ($evalType) {
            'range' => $this->evaluateRange((float) $value, $params),
            'categorical' => $this->evaluateCategorical((string) $value, $params),
            'distance_bonus' => $this->evaluateDistanceBonus((float) $value, $params),
            'distance_penalty' => $this->evaluateDistancePenalty((float) $value, $params),
            'trend' => $this->evaluateTrend((string) $value, $params),
            'gradient' => $this->evaluateGradient((float) $value, $params),
            'presence' => $this->evaluatePresence((string) $value, $params),
            'value_in_set' => $this->evaluateValueInSet($value, $params),
            default => 0.5,
        };
    }

    /** range: Optimal aralıkta max, dışına çıktıkça düşer. */
    public function evaluateRange(float $value, array $params): float
    {
        $optMin = $params['optimal_min'] ?? 0;
        $optMax = $params['optimal_max'] ?? 100;
        $accMin = $params['acceptable_min'] ?? $optMin;
        $accMax = $params['acceptable_max'] ?? $optMax;
        $falloff = $params['falloff'] ?? 'linear';

        if ($value >= $optMin && $value <= $optMax) {
            return 1.0;
        }
        if ($value < $accMin || $value > $accMax) {
            return 0.0;
        }

        $dist = $value < $optMin ? ($optMin - $value) : ($value - $optMax);
        $range = $value < $optMin ? ($optMin - $accMin) : ($accMax - $optMax);
        if ($range <= 0) {
            return 0.0;
        }

        $ratio = $dist / $range;

        return match ($falloff) {
            'gaussian' => exp(-3 * $ratio * $ratio),
            'quadratic' => max(0, 1 - $ratio * $ratio),
            default => max(0, 1 - $ratio),
        };
    }

    /** categorical: Kategori → puan tablosu. Bilinmeyen değerler nötr. */
    public function evaluateCategorical(string $value, array $params): float
    {
        $weights = $params['weights'] ?? [];
        if (! isset($weights[$value])) {
            return 0.5;
        } // bilinmeyen = nötr

        return (float) $weights[$value];
    }

    /** distance_bonus: Yakın = iyi, mesafeyle azalır. */
    public function evaluateDistanceBonus(float $distance, array $params): float
    {
        $bonusWithinM = $params['bonus_within_m'] ?? 1000;
        $maxBonus = $params['max_bonus'] ?? 1.0;
        $falloff = $params['falloff'] ?? 'linear';

        if ($distance <= 0) {
            return $maxBonus;
        }
        if ($distance >= $bonusWithinM) {
            return 0.0;
        }

        $ratio = $distance / $bonusWithinM;

        return match ($falloff) {
            'gaussian' => $maxBonus * exp(-3 * $ratio * $ratio),
            default => $maxBonus * (1 - $ratio),
        };
    }

    /** distance_penalty: Uzak = iyi (yapıdan kaçınan türler). */
    public function evaluateDistancePenalty(float $distance, array $params): float
    {
        $penaltyWithinM = $params['penalty_within_m'] ?? 500;
        if ($distance >= $penaltyWithinM) {
            return 1.0;
        }

        return $distance <= 0 ? 0.0 : $distance / $penaltyWithinM;
    }

    /** trend: Eğilim sınıfına göre puan. */
    public function evaluateTrend(string $trend, array $params): float
    {
        return (float) ($params['scores'][$trend] ?? 0.5);
    }

    /** gradient: Gradyan keskinliği. */
    public function evaluateGradient(float $gradient, array $params): float
    {
        $min = $params['min_gradient'] ?? 0;
        $max = $params['max_gradient'] ?? 1;
        if ($gradient <= $min) {
            return 0.0;
        }
        if ($gradient >= $max) {
            return 1.0;
        }

        return ($gradient - $min) / ($max - $min);
    }

    /** presence: Ağırlıklı varlık. */
    public function evaluatePresence(string $value, array $params): float
    {
        return (float) ($params['values'][$value] ?? 0.0);
    }

    /** value_in_set: Değer kümede mi? */
    public function evaluateValueInSet(mixed $value, array $params): float
    {
        $set = $params['set'] ?? [];

        return in_array($value, $set) ? (float) ($params['inside_score'] ?? 1.0) : (float) ($params['outside_score'] ?? 0.0);
    }

    // ──────────── Yardımcılar ────────────

    private function weightedAverage(array $subScores): float
    {
        if (empty($subScores)) {
            return 50.0;
        }
        $tw = 0;
        $ws = 0;
        foreach ($subScores as $e) {
            $tw += $e['sub_score'] * $e['weight'];
            $ws += $e['weight'];
        }

        return $ws > 0 ? ($tw / $ws) * 100 : 50.0;
    }

    private function calculatePreyMultiplier(Species $species, float $lat, float $lon): float
    {
        $preyRelations = $species->preyRelations;
        if ($preyRelations->isEmpty()) {
            return 1.0;
        }

        $tp = 0;
        $ts = 0;
        foreach ($preyRelations as $r) {
            try {
                $ps = $this->featureCalculator->getPreyPresenceScore($r->prey_species_id, $lat, $lon);
                $tp += $ps * $r->strength;
                $ts += $r->strength;
            } catch (\Exception) {
                continue;
            }
        }
        if ($ts <= 0) {
            return 1.0;
        }

        return 0.8 + (($tp / $ts) * 0.5);
    }

    private function isStaticFeature(string $key): bool
    {
        return in_array($key, [
            'depth_m', 'substrate_type', 'bottom_slope',
            'distance_to_coastline_m', 'distance_to_river_mouth_m',
            'distance_to_posidonia_edge_m', 'distance_to_rocky_structure_m',
            'distance_to_artificial_reef_m', 'distance_to_wreck_m',
            'water_column_zone', 'exposure', 'estuarine_influence',
            'protected_area_distance_m',
        ]);
    }

    private function generateExplanations(array $subScores): array
    {
        if (empty($subScores)) {
            return [];
        }

        usort($subScores, fn ($a, $b) => abs($b['weighted_contribution'] - 0.5) <=> abs($a['weighted_contribution'] - 0.5));

        $labels = [
            'depth_m' => 'Derinlik', 'substrate_type' => 'Dip tipi',
            'distance_to_river_mouth_m' => 'Dere ağzına mesafe',
            'distance_to_rocky_structure_m' => 'Kayalık yapıya mesafe',
            'distance_to_posidonia_edge_m' => 'Posidonia kenarına mesafe',
            'distance_to_wreck_m' => 'Batık mesafesi',
            'sst_c' => 'Deniz suyu sıcaklığı', 'chlorophyll_mg_m3' => 'Klorofil',
            'pressure_trend_6h' => 'Basınç eğilimi', 'wind_speed' => 'Rüzgar hızı',
            'wave_height_m' => 'Dalga yüksekliği', 'daylight_state' => 'Gün dilimi',
            'moon_phase' => 'Ay evresi', 'month_of_year' => 'Mevsim',
            'current_speed' => 'Akıntı hızı', 'baitfish_score' => 'Yem balığı',
            'water_column_zone' => 'Su kolonu', 'exposure' => 'Maruziyet',
            'estuarine_influence' => 'Acı su etkisi', 'sst_gradient' => 'Sıcaklık gradyanı',
        ];

        $result = [];
        foreach (array_slice($subScores, 0, 5) as $e) {
            $dir = $e['sub_score'] >= 0.6 ? '↑' : ($e['sub_score'] <= 0.4 ? '↓' : '→');
            $lbl = $labels[$e['feature_key']] ?? $e['feature_key'];
            $valueLabel = ($e['value'] === null || $e['value'] === 'unknown') ? 'Veri yok' : $e['value'];
            $txt = match (true) {
                $e['sub_score'] >= 0.8 => "{$lbl}: {$valueLabel} — ideal aralıkta",
                $e['sub_score'] >= 0.6 => "{$lbl}: {$valueLabel} — uygun",
                $e['sub_score'] >= 0.4 => "{$lbl}: {$valueLabel} — orta düzey",
                default => "{$lbl}: {$valueLabel} — tercih dışı",
            };
            $result[] = ['direction' => $dir, 'feature_key' => $e['feature_key'], 'sub_score' => round($e['sub_score'], 2), 'weight' => $e['weight'], 'text' => $txt];
        }

        return $result;
    }
}
