<?php

namespace App\Services;

use App\Facades\Settings;
use App\Metrics\EnquiryCount;
use App\Metrics\ServiceBookingCount;
use App\Metrics\VehicleCount;
use App\Metrics\VehiclesAdded;
use App\Metrics\VehiclesRemoved;
use App\Metrics\VehiclesReserved;
use App\Metrics\VehiclesSold;
use App\Support\Period;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Mtc\MercuryDataModels\Repositories\FeatureRepository;
use Mtc\MercuryDataModels\Tenant;
use Mtc\VehicleReservations\Reservation;

class GraphService
{
    private const COLOUR_NAMES = [
        'app-primary-colour',
        'app-secondary-colour',
        'app-tertiary-colour',
        'app-success-colour',
        'app-error-colour',
        'app-information-colour',
        'app-text-colour',
        'app-subtext-colour',
        'app-layer-dark-colour',
        'app-layer-accent-colour',
        'app-layer-light-colour',
    ];

    private const HIDDEN_DATA_ENTRIES = [
        'sum_visit_length',
        'nb_actions',
        'max_actions',
        'enquiries-previous-year',
    ];

    private Collection $data;

    public function __construct(
        private readonly FeatureRepository $features,
        private readonly Matomo $matomo,
    ) {
    }

    public function getGraph(array $data = []): array
    {
        try {
            $metricName = $data['metrics'] ?? 'leads';
            $dateRange = (int) ($data['date_range'] ?? 30) ?: 30;

            $result = $this->handle($metricName, $dateRange);

            $columns = $result->map(fn ($entry) => array_keys($entry))
                ->flatten(1)
                ->unique()
                ->reject(fn ($value) => $value === 'date');

            return [
                'status' => 'ok',
                'labels' => $result
                    ->sortBy('date')
                    ->pluck('date')
                    ->map(fn ($date) => Carbon::parse($date)->format('jS M')),
                'date_range' => $dateRange,
                'type' => $metricName,
                'datasets' => $columns
                    ->map(fn ($column, $index) => [
                        'label' => __("labels.$column"),
                        'backgroundColor' => Settings::get(self::COLOUR_NAMES[$index] ?? 'app-primary-colour'),
                        'data' => $result->sortBy('date')->pluck($column)->map(fn ($value) => $value ?? 0),
                        'tension' => 0.3,
                        'hidden' => $this->isHidden($column),
                    ])->values()->toArray(),
            ];
        } catch (\Exception $exception) {
            Log::error($exception->getMessage(), $exception->getTrace());
            return $this->graphError();
        }
    }

    public function visitorCount(Tenant $tenant, Carbon $date): int
    {
        $matomoSiteId = $tenant->matomo_site_id ?? null;

        if (!$matomoSiteId) {
            return 0;
        }

        try {
            return $this->matomo
                ->setSiteId($matomoSiteId)
                ->getVisitorsForDate($date->format('Y-m-d'));
        } catch (\Exception $e) {
            Log::warning("Failed to fetch visitor count for tenant $tenant->id: {$e->getMessage()}");
        }

        return 0;
    }

    private function handle(string $metricName, int $dateRange): Collection
    {
        $metrics = array_filter($this->getMetrics($metricName));
        $this->prepareDateRange(Period::days($dateRange));

        if ($this->requiresMatomo($metrics) && tenant('matomo_site_id')) {
            try {
                $this->addMatomoCollectionData($this->runAnalytics($metrics, $dateRange));
            } catch (\Exception) {
                // Ignore issue with Analytics
            }
        }

        $remainingMetrics = array_diff($metrics, Matomo::$allowedMetrics);
        if (!empty($remainingMetrics)) {
            collect($remainingMetrics)
                ->each(function ($metric) use ($dateRange) {
                    $metric_data = $this->runMetric($metric, $dateRange);
                    if ($metric_data->isNotEmpty()) {
                        $this->addToData($metric_data, $metric);
                    }
                });
        }

        return $this->data;
    }

    private function isHidden(string $dataEntry): bool
    {
        return in_array($dataEntry, self::HIDDEN_DATA_ENTRIES);
    }

    private function getMatomoMetricsToUse(array $allMetrics): array
    {
        return array_values(array_intersect(Matomo::$allowedMetrics, $allMetrics));
    }

    private function requiresMatomo(array $allMetrics): bool
    {
        return !empty($this->getMatomoMetricsToUse($allMetrics));
    }

    private function runAnalytics(array $allMetrics, int $dateRange)
    {
        return $this->matomo
            ->setSiteId(tenant('matomo_site_id'))
            ->runReport($dateRange, $this->getMatomoMetricsToUse($allMetrics));
    }

    private function runMetric(string $metric, int $dateRange): Collection
    {
        try {
            return match ($metric) {
                'enquiries' => $this->ensureFilledArray(
                    new EnquiryCount()->get(Period::days($dateRange)),
                    $dateRange
                ),

                'enquiries-previous-year' => $this->getEnquiriesPreviousYear($dateRange),

                'service-booking' => $this->ensureFilledArray(
                    new ServiceBookingCount()->get(Period::days($dateRange)),
                    $dateRange
                ),

                'vehicles' => $this->ensureFilledArray(
                    new VehicleCount()->get(Period::days($dateRange)),
                    $dateRange
                ),
                'vehicles-added' => $this->ensureFilledArray(
                    new VehiclesAdded()->get(Period::days($dateRange)),
                    $dateRange
                ),
                'vehicles-removed' => $this->ensureFilledArray(
                    new VehiclesRemoved()->get(Period::days($dateRange)),
                    $dateRange
                ),
                'vehicles-reserved' => $this->ensureFilledArray(
                    new VehiclesReserved()->get(Period::days($dateRange)),
                    $dateRange
                ),
                'vehicles-sold' => $this->ensureFilledArray(
                    new VehiclesSold()->get(Period::days($dateRange)),
                    $dateRange
                ),
                'reservations' => $this->ensureFilledArray($this->getReservationsForDateRange($dateRange), $dateRange),
                default => collect([]),
            };
        } catch (\Exception) {
            return collect([]);
        }
    }

    private function prepareDateRange(Period $period): void
    {
        $this->data = collect();
        $from = Carbon::parse($period->startDate);
        $to = Carbon::parse($period->endDate);
        $this->data->put(
            $from->format('Ymd'),
            ['date' => $from->format('Ymd')],
        );
        do {
            $from->addDay();
            $this->data->put(
                $from->format('Ymd'),
                ['date' => $from->format('Ymd')],
            );
        } while ($from->lt($to));
    }

    private function addToData(Collection $data, ?string $metric = null): void
    {
        $data->each(function ($value, $date) use ($metric) {
            $dateValue = Carbon::parse($date)->format('Ymd');
            $this->data->put($dateValue, array_merge(
                [$metric => $value],
                $this->data[$dateValue] ?? []
            ));
        });
    }

    private function addMatomoCollectionData(array $data): void
    {
        collect($data[0] ?? [])->each(function ($value, $date) {
            $dateValue = Carbon::parse($date)->format('Ymd');
            $this->data->put($dateValue, array_merge(
                $value,
                $this->data[$dateValue] ?? []
            ));
        });
    }

    private function ensureFilledArray(Collection $data, int $dateRange): Collection
    {
        return collect(range(0, $dateRange))
            ->map(fn ($daysInPast) => Carbon::today()->subDays($daysInPast))
            ->keyBy(fn (Carbon $date) => $date->format('Y-m-d'))
            ->map(fn (Carbon $date) => $data[$date->format('Y-m-d')] ?? 0);
    }

    private function getReservationsForDateRange(int $dateRange): Collection
    {
        return Reservation::query()
            ->whereNotNull('confirmed_at')
            ->where('confirmed_at', '>', Carbon::now()->startOfDay()->subDays($dateRange))
            ->where('confirmed_at', '<=', Carbon::now()->endOfDay())
            ->get()
            ->groupBy(fn ($reservation) => $reservation->confirmed_at->format('Y-m-d'))
            ->map(fn ($reservationsInDate) => $reservationsInDate->count());
    }

    private function getMetrics(string $metricName): array
    {
        return match ($metricName) {
            'visitors' => [
                'VisitsSummary.get',
            ],
            'leads' => array_filter([
                'enquiries',
                'sessions',
                $this->checkEnabled('reservations'),
                $this->checkEnabled('service-booking'),
                $this->checkLiveForYear() ? 'enquiries-previous-year' : null,
            ]),
            'vehicles' => [
                'vehicles',
                'vehicles-added',
                'vehicles-removed',
                'vehicles-reserved',
                'vehicles-sold',
            ],
            default => [],
        };
    }

    private function checkEnabled(string $feature): ?string
    {
        return $this->features->isEnabled($feature) ? $feature : null;
    }

    private function checkLiveForYear(): bool
    {
        $liveAt = tenant('live_at');

        if (!$liveAt) {
            return false;
        }

        return Carbon::parse($liveAt)->addYear()->isPast();
    }

    private function getEnquiriesPreviousYear(int $dateRange): Collection
    {
        $startDate = Carbon::today()->subDays($dateRange)->subYear();
        $endDate = Carbon::today()->subYear();

        $period = Period::create($startDate, $endDate);

        $data = new EnquiryCount()->get($period);

        $mapped = $data->mapWithKeys(function ($count, $date) {
            $currentYearDate = Carbon::parse($date)->addYear()->format('Y-m-d');
            return [$currentYearDate => $count];
        });

        return $this->ensureFilledArray($mapped, $dateRange);
    }

    private function graphError(): array
    {
        return [
            'status' => 'fail',
            'message' => 'Could not load stats for site visitors. Please ensure '
                . '<a href="/settings/site/seo">GA account is set up and access is granted</a>',
        ];
    }
}
