<?php

namespace Openguru\OpenCartFramework\Container;

use Openguru\OpenCartFramework\Events\EventDispatcher;
use Openguru\OpenCartFramework\Exceptions\ContainerDependencyResolutionException;
use Openguru\OpenCartFramework\Support\Arr;
use Phar;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

// phpcs:disable PSR1.Files.SideEffects
if (! defined('BP_BASE_PATH')) {
    $phar = Phar::running(false);
    $realBasePath = $phar ? dirname($phar) : dirname(__DIR__) . '/..';
    $pharBasePath = $phar ? "phar://$phar" : $realBasePath;

    define('BP_REAL_BASE_PATH', $realBasePath);
    define('BP_PHAR_BASE_PATH', $pharBasePath);
    define('BP_BASE_PATH', $pharBasePath);
}
// phpcs:enable PSR1.Files.SideEffects

class Container implements ContainerInterface
{
    private array $factories = [];
    private array $instances = [];
    private array $config;
    private array $taggedAbstracts = [];

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    public function getConfigValue(?string $key = null, $default = null)
    {
        if ($key === null) {
            return $this->config;
        }

        return Arr::get($this->config, $key, $default);
    }

    public function has(string $id): bool
    {
        return array_key_exists($id, $this->factories);
    }

    public function bind(string $abstract, callable $concrete, bool $singleton = false): void
    {
        $this->factories[$abstract] = [
            'concrete' => $concrete,
            'singleton' => $singleton,
        ];
    }

    public function singleton(string $abstract, callable $concrete): void
    {
        $this->bind($abstract, $concrete, true);
    }

    /**
     * @template T
     * @param class-string<T> $id
     * @return T
     */
    public function get(string $id)
    {
        try {
            if ($this->has($id)) {
                if ($this->factories[$id]['singleton']) {
                    if (! array_key_exists($id, $this->instances)) {
                        $this->instances[$id] = $this->factories[$id]['concrete']($this);
                    }

                    return $this->instances[$id];
                }

                return $this->factories[$id]['concrete']($this);
            }

            if (class_exists($id)) {
                $reflection = new ReflectionClass($id);
                $constructor = $reflection->getConstructor();

                $arguments = [];

                if ($constructor && $parameters = $constructor->getParameters()) {
                    foreach ($parameters as $parameter) {
                        if ($parameter->getType() instanceof ReflectionNamedType) {
                            $parameterType = $parameter->getType()->getName();
                            /** @psalm-suppress ArgumentTypeCoercion */
                            $arguments[] = $this->get($parameterType);
                        } else {
                            $arguments[] = null;
                        }
                    }
                }

                /** @var T $instance */
                $instance = $reflection->newInstanceArgs($arguments);

                return $instance;
            }
        } catch (ReflectionException $exception) {
            throw new ContainerDependencyResolutionException(
                'Could not resolve the concrete: ' . $id . '. ' . $exception->getMessage()
            );
        }

        throw new ContainerDependencyResolutionException(
            'Could not resolve the concrete: ' . $id . ' (class does not exist)'
        );
    }

    /**
     * @param class-string $abstract
     * @throws ReflectionException
     */
    public function call(string $abstract, string $method): Response
    {
        if (! class_exists($abstract)) {
            throw new ContainerDependencyResolutionException('Could not resolve the concrete: ' . $abstract);
        }

        if (! method_exists($abstract, $method)) {
            throw new ContainerDependencyResolutionException('Method not found: ' . $abstract . '@' . $method);
        }

        $instance = $this->get($abstract);

        $reflection = new ReflectionMethod($instance, $method);

        $arguments = [];
        foreach ($reflection->getParameters() as $parameter) {
            if ($parameter->getType() instanceof ReflectionNamedType) {
                $parameterType = $parameter->getType()->getName();
                /** @psalm-suppress ArgumentTypeCoercion */
                $arguments[] = $this->get($parameterType);
            } else {
                $arguments[] = null;
            }
        }

        return $instance->{$method}(...$arguments);
    }

    public function registerEventListener(string $eventClass, string $listenerClass): void
    {
        $dispatcher = $this->get(EventDispatcher::class);

        $dispatcher->registerListener($eventClass, $listenerClass);
    }

    public function initializeEventDispatcher(array $listeners): void
    {
        $this->singleton(EventDispatcher::class, function (Container $container) {
            return new EventDispatcher($container);
        });

        foreach ($listeners as $eventClass => $listenerClasses) {
            foreach ($listenerClasses as $listenerClass) {
                $this->registerEventListener($eventClass, $listenerClass);
            }
        }
    }

    public function tag(string $tag, array $abstracts): void
    {
        if (array_key_exists($tag, $this->taggedAbstracts)) {
            throw new RuntimeException('Tag already exists: ' . $tag);
        }

        $this->taggedAbstracts[$tag] = $abstracts;
    }
}
