<?php

namespace Dedoc\Scramble\Support\InferExtensions;

use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
use Dedoc\Scramble\Infer\Extensions\MethodReturnTypeExtension;
use Dedoc\Scramble\Support\Type\Generic;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class EloquentBuilderExtension implements MethodReturnTypeExtension
{
    public function shouldHandle(ObjectType $type): bool
    {
        return $type->isInstanceOf(Builder::class);
    }

    public function getMethodReturnType(MethodCallEvent $event): ?Type
    {
        if ($event->getDefinition()->hasMethodDefinition($event->getName())) {
            return $this->handleExistingMethodReturnType($event);
        }

        if ($this->shouldForwardCallToModel($event)) {
            return $this->forwardCallToModel($event);
        }

        if (! $modelType = $this->getModelType($event->getInstance())) {
            return null;
        }
        if (
            $this->modelIsSoftDeletable($modelType)
            && ($softDeleteCallResult = $this->handleSoftDeletes($event, $modelType))
        ) {
            return $softDeleteCallResult;
        }

        return null;
    }

    private function handleExistingMethodReturnType(MethodCallEvent $event): ?Type
    {
        return match ($event->getName()) {
            'get' => ($modelType = $this->getModelType($event->getInstance()))
                ? ModelCollectionTypeResolver::resolve($modelType)
                : null,
            default => null,
        };
    }

    private function shouldForwardCallToModel(MethodCallEvent $event): bool
    {
        if (! $modelType = $this->getModelType($event->getInstance())) {
            return false;
        }

        return $event->scope->index->getClass($modelType->name)?->hasMethodDefinition(
            $this->getScopeMethodName($event->name),
        ) ?? false;
    }

    private function forwardCallToModel(MethodCallEvent $event): ?Type
    {
        if (! $modelType = $this->getModelType($event->getInstance())) {
            return null;
        }

        return $event->getInstance();
    }

    /**
     * @see SoftDeletes
     */
    private function modelIsSoftDeletable(ObjectType $modelType): bool
    {
        return method_exists($modelType->name, 'bootSoftDeletes');
    }

    private function handleSoftDeletes(MethodCallEvent $event, ObjectType $modelType): ?Type
    {
        return match ($event->name) {
            'onlyTrashed', 'withTrashed', 'withoutTrashed' => $event->getInstance(),
            'restoreOrCreate', 'createOrRestore' => $modelType,
            default => null,
        };
    }

    private function getModelType(ObjectType $instance): ?ObjectType
    {
        $type = $this->normalizeType($instance);

        $modelType = $type->templateTypes[0] ?? null;

        if (! $modelType instanceof ObjectType) {
            return null;
        }

        if (! $modelType->isInstanceOf(Model::class)) {
            return null;
        }

        return $modelType;
    }

    private function normalizeType(ObjectType $type): Generic
    {
        if ($type instanceof Generic) {
            return $type;
        }

        return new Generic($type->name, []);
    }

    private function getScopeMethodName(string $scope): string
    {
        return 'scope'.ucfirst($scope);
    }
}
