<?php
namespace Nibiru\Module\Ai;

/**
 * Nibiru AI module — the framework's first-class AI surface.
 *
 *   $ai = new \Nibiru\Module\Ai\Ai();
 *   echo $ai->chat()->ask('Explain the dispatcher in two sentences.');
 *
 *   $rag = $ai->rag('docs');
 *   $rag->ingest(__DIR__ . '/../../view/templates/');
 *   echo $rag->ask('Where is the login form built?');
 *
 *   $agent = $ai->agent()->withTools([
 *       new Plugins\Tools\PdoQuery(),
 *       new Plugins\Tools\HttpGet(),
 *   ]);
 *   echo $agent->run('How many active users do we have?');
 *
 * Configuration in application/module/ai/settings/ai.ini.
 *
 * @author Stephan Kasdorf
 * @license BSD
 */

use Nibiru\Module\Ai\Interfaces;
use Nibiru\Module\Ai\Traits;
use Nibiru\Module\Ai\Plugins;
use Nibiru\Registry;
use SplSubject;
use SplObserver;
use SplObjectStorage;

class Ai implements Interfaces\Ai, SplSubject
{
    use Traits\Ai;

    const CONFIG_MODULE_NAME = 'ai';

    /** @var \stdClass module config from settings/ai.ini */
    protected static ?\stdClass $aiRegistry = null;

    /** @var SplObjectStorage observer storage */
    protected SplObjectStorage $observers;

    /** @var Plugins\Chat|null lazy chat plugin */
    protected ?Plugins\Chat $chatPlugin = null;

    /** @var Plugins\Embed|null lazy embed plugin */
    protected ?Plugins\Embed $embedPlugin = null;

    /** @var array<string, Plugins\Rag> RAG instances keyed by collection name */
    protected array $ragInstances = [];

    public function __construct()
    {
        $this->setAiRegistry();
        $this->observers = new SplObjectStorage();
    }

    public function attach(SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }

    public function detach(SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }

    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * Get a chat-completion plugin.
     */
    public function chat(): Plugins\Chat
    {
        return $this->chatPlugin ??= new Plugins\Chat($this->config());
    }

    /**
     * Get an embedding plugin.
     */
    public function embed(): Plugins\Embed
    {
        return $this->embedPlugin ??= new Plugins\Embed($this->config());
    }

    /**
     * Get a named RAG (Retrieval-Augmented Generation) collection. Each
     * collection has its own on-disk JSON vector index, so you can have
     * one RAG over your docs, another over your error logs, another over
     * customer-support tickets, all in the same app.
     */
    public function rag(string $collection = 'default'): Plugins\Rag
    {
        return $this->ragInstances[$collection] ??= new Plugins\Rag(
            $collection,
            $this->config(),
            $this->chat(),
            $this->embed()
        );
    }

    /**
     * Get an agent with optional tools attached. Agents call the LLM
     * iteratively with tool-call decoding until they hit a terminal answer.
     */
    public function agent(): Plugins\Agent
    {
        return new Plugins\Agent($this->config(), $this->chat());
    }

    /**
     * The active config (a stdClass populated from settings/ai.ini).
     */
    public function config(): \stdClass
    {
        return self::$aiRegistry;
    }

    protected function setAiRegistry(): void
    {
        if (self::$aiRegistry === null) {
            $cfg = Registry::getInstance()->loadModuleConfigByName(self::CONFIG_MODULE_NAME);
            self::$aiRegistry = $cfg ?: new \stdClass();
        }
    }
}
