<?php

namespace KimaAI\Lib;

use Exception;
use KimaAI\Exceptions\ForbiddenPropertyMutationException;
use KimaAI\Loader;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Abstract class to extend for the main class in a plugin.
 */
abstract class AbstractMain {

  /**
   * The DI container.
   *
   * @var ContainerInterface|null
   */
  protected $container;

  /**
   * A cache for previously requested and constructed surfaces.
   *
   * @var mixed[]
   */
  private $cached_surfaces = [];

  /**
   * Loads the plugin.
   *
   * @return void
   *
   * @throws Exception If loading fails and KIMAAI_ENVIRONMENT is development.
   */
  public function load() {
    if ($this->container) {
      return;
    }

    try {
      $this->container = $this->getContainer();
      ContainerRegistry::register($this->getName(), $this->container);

      if (!$this->container) {
        return;
      }
      if (!$this->container->has(Loader::class)) {
        return;
      }

      $this->container->get(Loader::class)->load();
    } catch (Exception $exception) {
      if ($this->isDevelopment()) {
        throw $exception;
      }
      // Don't crash the entire site, simply don't load.
    }
  }

  /**
   * Magic getter for retrieving a property from a surface.
   *
   * @param string $property The property to retrieve.
   *
   * @return mixed The value of the property.
   *
   * @throws Exception When the property doesn't exist.
   */
  public function __get($property) {
    if (\array_key_exists($property, $this->cached_surfaces)) {
      return $this->cached_surfaces[$property];
    }

    $surfaces = $this->getSurfaces();

    if (isset($surfaces[$property])) {
      $this->cached_surfaces[$property] = $this->container->get($surfaces[$property]);

      return $this->cached_surfaces[$property];
    }
    throw new Exception(\sprintf('Property $%s does not exist.', esc_html($property)));
  }

  /**
   * Checks if the given property exists as a surface.
   *
   * @param string $property The property to retrieve.
   *
   * @return bool True when property is set.
   */
  public function __isset($property) {
    if (\array_key_exists($property, $this->cached_surfaces)) {
      return true;
    }

    $surfaces = $this->getSurfaces();

    if (!isset($surfaces[$property])) {
      return false;
    }

    return $this->container->has($surfaces[$property]);
  }

  /**
   * Prevents setting dynamic properties and unsetting declared properties
   * from an inaccessible context.
   *
   * @param string $name The property name.
   * @param mixed $value The property value.
   *
   * @return void
   *
   * @throws ForbiddenPropertyMutationException Set is never meant to be called.
   */
  public function __set(
    $name,
    $value // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- __set must have a name and value - PHPCS #3715.
  ) {
    throw ForbiddenPropertyMutationException::cannotSetBecausePropertyIsImmutable(esc_html($name));
  }

  /**
   * Prevents unsetting dynamic properties and unsetting declared properties
   * from an inaccessible context.
   *
   * @param string $name The property name.
   *
   * @return void
   *
   * @throws ForbiddenPropertyMutationException Unset is never meant to be called.
   */
  public function __unset($name) {
    throw ForbiddenPropertyMutationException::cannotUnsetBecausePropertyIsImmutable(esc_html($name));
  }

  /**
   * Loads the DI container.
   *
   * @return ContainerInterface|null The DI container.
   *
   * @throws Exception If something goes wrong generating the DI container.
   */
  abstract protected function getContainer();

  /**
   * Gets the name of the plugin.
   *
   * @return string The name.
   */
  abstract protected function getName();

  /**
   * Gets the surfaces of this plugin.
   *
   * @return array A mapping of surface name to the responsible class.
   */
  abstract protected function getSurfaces();

  /**
   * Returns whether or not we're in an environment for KimaAI development.
   *
   * @return bool Whether or not to load in development mode.
   */
  protected function isDevelopment() {
    return \defined('KIMAAI_ENVIRONMENT') && \KIMAAI_ENVIRONMENT === 'development';
  }
}
