<?php

namespace App\Http\Controllers;

use App\Http\Requests\MealPostCalendarRequest;
use App\Http\Requests\MealPostsRequest;
use App\Http\Requests\MealPostStatsRequest;
use App\Http\Resources\MealPostsResource;
use App\Models\MealPosts;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class MealPostController extends Controller
{
    public function index(Request $request): AnonymousResourceCollection
    {
        $perPage = max(1, min($request->integer('per_page', 15), 100));

        $mealPosts = $this->mealPostListQuery($request)
            ->latest('eaten_at')
            ->paginate($perPage)
            ->withQueryString();

        return MealPostsResource::collection($mealPosts);
    }

    public function mine(Request $request): AnonymousResourceCollection
    {
        $perPage = max(1, min($request->integer('per_page', 15), 100));

        $mealPosts = $this->mealPostListQuery($request)
            ->where('user_id', $request->user()->getKey())
            ->latest('eaten_at')
            ->paginate($perPage)
            ->withQueryString();

        return MealPostsResource::collection($mealPosts);
    }

    public function stats(MealPostStatsRequest $request): JsonResponse
    {
        $validated = $request->validated();
        $period = $validated['period'] ?? 'day';
        $referenceDate = CarbonImmutable::createFromFormat('!Y-m-d', $validated['date'] ?? now()->toDateString());

        [$startDate, $endDate] = match ($period) {
            'week' => [
                $referenceDate->startOfWeek(CarbonInterface::MONDAY),
                $referenceDate->endOfWeek(CarbonInterface::SUNDAY),
            ],
            default => [
                $referenceDate->startOfDay(),
                $referenceDate->endOfDay(),
            ],
        };

        $mealPosts = MealPosts::query()
            ->where('user_id', $request->user()->getKey())
            ->whereBetween('eaten_at', [$startDate, $endDate])
            ->get(['calories', 'proteins', 'carbs', 'fats', 'eaten_at']);

        return response()->json([
            'data' => [
                'period' => $period,
                'date' => $referenceDate->toDateString(),
                'startDate' => $startDate->toDateString(),
                'endDate' => $endDate->toDateString(),
                'totals' => $this->getNutritionTotals($mealPosts),
                'days' => $this->getDailyNutritionTotals($mealPosts, $startDate, $endDate),
            ],
        ]);
    }

    public function calendar(MealPostCalendarRequest $request): JsonResponse
    {
        $month = $request->validated('month') ?? now()->format('Y-m');
        $referenceDate = CarbonImmutable::createFromFormat('!Y-m', $month);
        $startDate = $referenceDate->startOfMonth();
        $endDate = $referenceDate->endOfMonth();

        $days = MealPosts::query()
            ->where('user_id', $request->user()->getKey())
            ->whereBetween('eaten_at', [$startDate->startOfDay(), $endDate->endOfDay()])
            ->selectRaw('DATE(eaten_at) as date, COUNT(*) as meals_count')
            ->groupByRaw('DATE(eaten_at)')
            ->orderBy('date')
            ->get()
            ->map(fn (MealPosts $mealPost): array => [
                'date' => CarbonImmutable::parse((string) $mealPost->date)->toDateString(),
                'mealsCount' => (int) $mealPost->meals_count,
            ])
            ->values();

        return response()->json([
            'data' => [
                'month' => $referenceDate->format('Y-m'),
                'startDate' => $startDate->toDateString(),
                'endDate' => $endDate->toDateString(),
                'days' => $days,
            ],
        ]);
    }

    public function store(MealPostsRequest $request): JsonResponse
    {
        $data = $this->getValidatedDataWithImageUrl($request);
        $ingredients = $data['ingredients'] ?? [];

        unset($data['ingredients']);

        $mealPost = DB::transaction(function () use ($data, $ingredients, $request): MealPosts {
            $mealPost = MealPosts::create([
                ...$data,
                'user_id' => $request->user()->getKey(),
            ]);

            if ($ingredients !== []) {
                $mealPost->ingredients()->createMany($ingredients);
            }

            return $mealPost;
        });

        $mealPost->setRelation('user', $request->user());

        return (new MealPostsResource($this->loadMealPostForResponse($mealPost, $request)))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Request $request, MealPosts $mealPost): MealPostsResource
    {
        $this->abortIfUnavailable($mealPost);

        return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
    }

    public function update(MealPostsRequest $request, MealPosts $mealPost): MealPostsResource
    {
        $this->abortIfNotOwner($request, $mealPost);

        $data = $this->getValidatedDataWithImageUrl($request);
        $shouldSyncIngredients = array_key_exists('ingredients', $data);
        $ingredients = $data['ingredients'] ?? [];

        unset($data['ingredients']);

        $mealPost = DB::transaction(function () use ($data, $ingredients, $mealPost, $shouldSyncIngredients): MealPosts {
            if ($data !== []) {
                $mealPost->update($data);
            }

            if ($shouldSyncIngredients) {
                $mealPost->ingredients()->delete();

                if ($ingredients !== []) {
                    $mealPost->ingredients()->createMany($ingredients);
                }
            }

            return $mealPost->refresh();
        });

        return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
    }

    public function like(Request $request, MealPosts $mealPost): MealPostsResource
    {
        $this->abortIfUnavailable($mealPost);

        $mealPost->likedByUsers()->syncWithoutDetaching([$request->user()->getKey()]);

        return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
    }

    public function unlike(Request $request, MealPosts $mealPost): MealPostsResource
    {
        $this->abortIfUnavailable($mealPost);

        $mealPost->likedByUsers()->detach($request->user()->getKey());

        return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
    }

    public function destroy(Request $request, MealPosts $mealPost): Response
    {
        $this->abortIfNotOwner($request, $mealPost);

        $mealPost->delete();

        return response()->noContent();
    }

    private function abortIfNotOwner(Request $request, MealPosts $mealPost): void
    {
        abort_unless($request->user(), 401);
        abort_unless($mealPost->user_id === $request->user()->getKey(), 404);
    }

    private function abortIfUnavailable(MealPosts $mealPost): void
    {
        abort_if(
            $mealPost->isHidden() || $mealPost->user()->whereNotNull('suspended_at')->exists(),
            404,
        );
    }

    private function loadMealPostForResponse(MealPosts $mealPost, Request $request): MealPosts
    {
        return $mealPost
            ->loadMissing('user:id,name,avatar_url,account_verified_at', 'ingredients');
    }

    private function mealPostListQuery(Request $request): Builder
    {
        return MealPosts::query()
            ->whereNull('hidden_at')
            ->whereHas('user', fn (Builder $query): Builder => $query->whereNull('suspended_at'))
            ->with('user:id,name,avatar_url,account_verified_at');
    }

    private function getValidatedDataWithImageUrl(MealPostsRequest $request): array
    {
        $data = $request->validated();

        if ($request->hasFile('image')) {
            $path = $request->file('image')->store('meal-posts');
            $data['image_url'] = $path;
        }

        unset($data['image']);

        return $data;
    }

    private function getDailyNutritionTotals(Collection $mealPosts, CarbonImmutable $startDate, CarbonImmutable $endDate): array
    {
        $days = [];

        for ($date = $startDate; $date->lte($endDate); $date = $date->addDay()) {
            $day = $date->toDateString();
            $dayMealPosts = $mealPosts->filter(
                fn (MealPosts $mealPost): bool => $mealPost->eaten_at->toDateString() === $day
            );

            $days[] = [
                'date' => $day,
                'totals' => $this->getNutritionTotals($dayMealPosts),
            ];
        }

        return $days;
    }

    private function getNutritionTotals(Collection $mealPosts): array
    {
        return [
            'calories' => (int) $mealPosts->sum(fn (MealPosts $mealPost): int => (int) ($mealPost->calories ?? 0)),
            'proteins' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->proteins ?? 0)), 2),
            'carbs' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->carbs ?? 0)), 2),
            'fats' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->fats ?? 0)), 2),
            'mealsCount' => $mealPosts->count(),
        ];
    }
}
