<?php

namespace ZenoChatbot\Services\LLMService\AIClient\Builders;

use ZenoChatbot\Services\LLMService\AIClient\Config\AiConfig;
use ZenoChatbot\Services\LLMService\AIClient\Config\ConfigBuilder;
use ZenoChatbot\Services\LLMService\AIClient\Results\GenerativeAiResult;
use ZenoChatbot\Services\LLMService\AIClient\Support\ResourceLoader;
use ZenoChatbot\Services\LLMService\AIClient\Support\ResourceResult;

if (!defined('ABSPATH')) {
    exit;
}

class PromptBuilder
{
    private ?AiConfig $config;
    private string $text;

    private ?string $providerOverride = null;
    private ?string $modelOverride = null;
    private ?float $temperatureOverride = null;
    private ?int $maxTokensOverride = null;
    private ?int $candidateCount = null;

    private array $systemPrompts = [];
    private array $context = [];
    private $resources = [];
    private ?string $persona = null;
    private array $personaHints = [];
    private bool $strict = false;
    private ?string $fallback = null;
    private bool $requireCitations = false;

    /** @var ResourceResult|null Loaded resources result with files and skipped info */
    private ?ResourceResult $loadedResourceResult = null;

    public function __construct(?AiConfig $config, string $text)
    {
        $this->config = $config;
        $this->text = $text;
    }

    /** @return self */
    public function usingPersona(string $name, array $styleHints = []): self
    {
        $this->persona = $name;
        $this->personaHints = $styleHints;
        return $this;
    }

    /** @return self */
    public function restrictToResources(bool $strict = true, ?string $fallback = null): self
    {
        $this->strict = $strict;
        $this->fallback = $fallback;
        return $this;
    }

    /**
     * Set chat history. Expects an array of turns with keys: message (string) and role ('user'|'assistant').
     */
    /** @return self */
    public function history(array $turns): self
    {
        // Store as-is; adapters can handle either 'role' or legacy 'sender_type'.
        $this->context = [];
        foreach ($turns as $t) {
            if (!is_array($t)) {
                continue;
            }
            $msg = (string)($t['message'] ?? '');
            $role = strtolower((string)($t['role'] ?? 'user'));
            if ($msg === '') {
                continue;
            }
            if ($role !== 'assistant' && $role !== 'user' && $role !== 'model') {
                $role = 'user';
            }
            $this->context[] = ['message' => $msg, 'role' => $role];
        }
        return $this;
    }

    /**
     * Add resources (paths, URLs, inline text, or true for defaults). Options: allowlist, max_bytes, timeout.
     */
    /** @return self */
    public function resources($resources, array $options = []): self
    {
        $this->resources = [$resources, $options];
        return $this;
    }

    /**
     * Define system prompt(s). Replaces existing system prompts.
     *
     * @param string|array $system
     * @return self
     */
    public function systemPrompt($system): self
    {
        $this->systemPrompts = is_array($system) ? $system : [$system];
        return $this;
    }

    /**
     * Add an application-level system prompt (appended to system prompts).
     *
     * @param string|array $system
     * @return self
     */
    public function applicationPrompt($system): self
    {
        $extra = is_array($system) ? $system : [$system];
        $this->systemPrompts = array_values(array_filter(array_merge($this->systemPrompts, $extra), static fn($s) => is_string($s) && trim($s) !== ''));
        return $this;
    }

    /**
     * Ask the model to include inline citations like [source: <name>].
     * Note: This nudges model behavior via prompt and also enables citation extraction from outputs.
     */
    /** @return self */
    public function requireCitations(bool $on = true): self
    {
        $this->requireCitations = $on;
        return $this;
    }


    public function generateText(): string
    {
        return $this->generateTextResult()->toText();
    }


    public function generateTextResult(): GenerativeAiResult
    {
        $base = $this->config ? $this->config->toArray() : [];
        $data = [
            'provider'    => $this->providerOverride ?: ($base['provider'] ?? ''),
            'model'       => $this->modelOverride ?: ($base['model'] ?? ''),
            'api_key'     => $base['api_key'] ?? '',
            'temperature' => $this->temperatureOverride ?? ($base['temperature'] ?? 0.7),
            'max_tokens'  => $this->maxTokensOverride ?? ($base['max_tokens'] ?? 1000),
        ];
        $effConfig = ConfigBuilder::create()->fromArray($data)->build();

        $system = $this->systemPrompts;
        if ($this->persona) {
            $style = empty($this->personaHints) ? '' : (' ' . implode(' ', $this->personaHints));
            array_unshift($system, sprintf('You are %s, a helpful support agent.%s', $this->persona, $style));
        }
        if ($this->strict) {
            $fallback = $this->fallback ?: 'Sorry, I can only answer from the provided resources.';
            $system[] = 'Answer ONLY using the provided resources. If not present, reply exactly: ' . $fallback;
        }
        if ($this->requireCitations) {
            $system[] = 'Cite sources inline as: [source: <name>] where appropriate.';
        }

        // Load resources with file support
        $resourcesSpec = $this->resources;
        $textResources = [];
        $fileResources = [];
        $skippedFiles = [];

        if (!empty($resourcesSpec)) {
            [$spec, $opts] = is_array($resourcesSpec) ? $resourcesSpec : [$resourcesSpec, []];
            $this->loadedResourceResult = ResourceLoader::loadWithFiles($spec, $opts);
            $textResources = $this->loadedResourceResult->getTextResources();
            $fileResources = $this->loadedResourceResult->getFileResources();
            $skippedFiles = $this->loadedResourceResult->getSkippedFiles();
        }

        // Add file resource instruction when files are present
        if (!empty($fileResources)) {
            $system[] = $this->getFileResourceInstruction();
        }

        $payload = [
            'message'           => $this->text,
            'system_prompts'    => $system,
            'context'           => $this->context,
            'resources'         => $textResources,
            'file_resources'    => $fileResources,
            'require_citations' => $this->requireCitations,
            'fallback'          => $this->fallback,
            'candidate_count'   => $this->candidateCount ?? 1,
            'temperature'       => $effConfig->getTemperature(),
            'max_tokens'        => $effConfig->getMaxTokens(),
        ];

        $providerClass = $effConfig->getProviderClass();
        $provider = new $providerClass();
        $result = $provider->text()->generate($payload, $effConfig);

        // Inject skipped files into result if any
        if (!empty($skippedFiles)) {
            return GenerativeAiResult::create([
                'candidates'    => array_map(
                    fn($t) => new \ZenoChatbot\Services\LLMService\AIClient\Results\Candidate($t, $result->getFinishReason()),
                    $result->toTexts()
                ),
                'token_usage'   => $result->getTokenUsage(),
                'finish_reason' => $result->getFinishReason(),
                'citations'     => $result->getCitations(),
                'provider'      => $result->getProvider(),
                'model'         => $result->getModel(),
                'raw_response'  => $result->getRawResponse(),
                'raw_request'   => $result->getRawRequest(),
                'skipped_files' => $skippedFiles,
            ]);
        }

        return $result;
    }

    /**
     * Get instruction for AI when file resources (images/PDFs) are present.
     */
    private function getFileResourceInstruction(): string
    {
        return 'You have access to uploaded reference materials. Follow these guidelines: '
            . 'Read and understand the content of these materials. '
            . 'Use this content to answer questions when relevant. '
            . 'If materials are not relevant to the question, use other resources or your knowledge. '
            . 'Do not mention or disclose the existence of these reference materials. '
            . 'Do not describe the materials unless the user specifically asks about their content.';
    }
}
