<?php

namespace App\Services;

use App\Facades\Settings;
use App\Metrics\EnquiryCount;
use App\Metrics\ServiceBookingCount;
use App\Metrics\VehicleCount;
use App\Metrics\VehicleViews;
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',
    ];

    // Restrict a graph to specific data columns. The visitors graph intentionally returns every
    // Matomo column; the dashboard picks a single series client-side via the metric dropdown.
    private const VISIBLE_COLUMNS = [];

    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';
            $period = $this->resolvePeriod($data);

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

            // The total vehicle count comes from an end-of-day reporting stat
            // (generated at midnight for the previous day), so the current day is
            // always empty. Drop today's point from the total-vehicles series so
            // the line doesn't dip to zero. This is a no-op when the selected
            // range doesn't reach today. Other vehicle series (added/removed/
            // reserved) reflect same-day activity, so they keep today.
            if (in_array($metricName, ['vehicles', 'vehicles-views'])) {
                $today = Carbon::today()->format('Ymd');
                $result = $result->reject(fn ($entry) => ($entry['date'] ?? null) === $today);
            }


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

            if (isset(self::VISIBLE_COLUMNS[$metricName])) {
                $columns = $columns
                    ->filter(fn ($column) => in_array($column, self::VISIBLE_COLUMNS[$metricName], true))
                    ->values();
            }

            return [
                'status' => 'ok',
                'labels' => $result
                    ->sortBy('date')
                    ->pluck('date')
                    ->map(fn ($date) => Carbon::parse($date)->format('jS M')),
                'date_range' => $this->periodDays($period),
                'from' => Carbon::parse($period->startDate)->format('Y-m-d'),
                'to' => Carbon::parse($period->endDate)->format('Y-m-d'),
                'type' => $metricName,
                'datasets' => $columns
                    ->map(function ($column, $index) use ($result) {
                        $colour = Settings::get(self::COLOUR_NAMES[$index] ?? 'app-primary-colour');

                        return [
                            'label' => __("labels.$column"),
                            'borderColor' => $colour,
                            'backgroundColor' => $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, Period $period): Collection
    {
        $metrics = array_filter($this->getMetrics($metricName));
        $this->prepareDateRange($period);

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

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

        return $this->data;
    }

    private function resolvePeriod(array $data): Period
    {
        if (!empty($data['from_date']) && !empty($data['to_date'])) {
            return Period::create(
                Carbon::parse($data['from_date'])->startOfDay(),
                Carbon::parse($data['to_date'])->endOfDay(),
            );
        }

        $dateRange = (int) ($data['date_range'] ?? 30) ?: 30;

        return Period::days($dateRange);
    }

    private function periodDays(Period $period): int
    {
        return (int) Carbon::parse($period->startDate)->startOfDay()
            ->diffInDays(Carbon::parse($period->endDate)->startOfDay());
    }

    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, Period $period)
    {
        return $this->matomo
            ->setSiteId(tenant('matomo_site_id'))
            ->runReport($period, $this->getMatomoMetricsToUse($allMetrics));
    }

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

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

                'service-booking' => $this->ensureFilledArray(
                    new ServiceBookingCount()->get($period),
                    $period
                ),

                'vehicles' => $this->ensureFilledArray(
                    new VehicleCount()->get($period),
                    $period
                ),
                'vehicles-added' => $this->ensureFilledArray(
                    new VehiclesAdded()->get($period),
                    $period
                ),
                'vehicles-removed' => $this->ensureFilledArray(
                    new VehiclesRemoved()->get($period),
                    $period
                ),
                'vehicles-reserved' => $this->ensureFilledArray(
                    new VehiclesReserved()->get($period),
                    $period
                ),
                'vehicles-sold' => $this->ensureFilledArray(
                    new VehiclesSold()->get($period),
                    $period
                ),
                'vehicles-views' => $this->ensureFilledArray(
                    new VehicleViews()->get($period),
                    $period
                ),
                'reservations' => $this->ensureFilledArray($this->getReservationsForPeriod($period), $period),
                default => collect([]),
            };
        } catch (\Exception) {
            return collect([]);
        }
    }

    private function prepareDateRange(Period $period): void
    {
        $this->data = collect();
        $date = Carbon::parse($period->startDate)->startOfDay();
        $end = Carbon::parse($period->endDate)->startOfDay();

        while ($date->lte($end)) {
            $this->data->put(
                $date->format('Ymd'),
                ['date' => $date->format('Ymd')],
            );
            $date->addDay();
        }
    }

    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, Period $period): Collection
    {
        $days = collect();
        $date = Carbon::parse($period->startDate)->startOfDay();
        $end = Carbon::parse($period->endDate)->startOfDay();

        while ($date->lte($end)) {
            $key = $date->format('Y-m-d');
            $days->put($key, $data[$key] ?? 0);
            $date->addDay();
        }

        return $days;
    }

    private function getReservationsForPeriod(Period $period): Collection
    {
        return Reservation::query()
            ->whereNotNull('confirmed_at')
            ->where('confirmed_at', '>=', Carbon::parse($period->startDate)->startOfDay())
            ->where('confirmed_at', '<=', Carbon::parse($period->endDate)->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',
            ],
            'enquiries' => ['enquiries'],
            'service-booking' => ['service-booking'],
            'reservations' => ['reservations'],
            'vehicles' => ['vehicles'],
            'vehicles-added' => ['vehicles-added'],
            'vehicles-removed' => ['vehicles-removed'],
            'vehicles-reserved' => ['vehicles-reserved'],
            'vehicles-views' => ['vehicles-views'],
            'leads' => array_filter([
                'enquiries',
                'sessions',
                $this->checkEnabled('reservations'),
                $this->checkEnabled('service-booking'),
                $this->checkLiveForYear() ? 'enquiries-previous-year' : null,
            ]),
            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(Period $period): Collection
    {
        $previousPeriod = Period::create(
            Carbon::parse($period->startDate)->subYear(),
            Carbon::parse($period->endDate)->subYear(),
        );

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

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

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

    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>',
        ];
    }
}
