<?php

namespace App\Domain\Memory\Actions;

use App\Domain\Agent\Models\Agent;
use App\Domain\Agent\Models\AgentExecution;
use App\Domain\Experiment\Models\Experiment;
use App\Domain\Memory\Enums\MemoryBeliefStatus;
use App\Domain\Memory\Enums\MemoryBeliefType;
use App\Domain\Memory\Enums\MemoryCategory;
use App\Domain\Memory\Enums\MemoryPreferenceSubtype;
use App\Domain\Memory\Enums\MemoryTier;
use App\Domain\Shared\Models\Team;
use App\Infrastructure\AI\Contracts\AiGatewayInterface;
use App\Infrastructure\AI\DTOs\AiRequestDTO;
use App\Infrastructure\AI\Services\ProviderResolver;
use Illuminate\Support\Facades\Log;

/**
 * Extracts durable facts from a completed AgentExecution using the configured
 * memory-extraction model (config('memory.extraction.model')).
 *
 * Facts are filtered by confidence >= 0.5 and stored as individual Memory records
 * via StoreMemoryAction, enriched with confidence scores and semantic tags.
 */
class ExtractAndStoreMemoriesAction
{
    private const MIN_CONFIDENCE = 0.5;

    private const SYSTEM_PROMPT = <<<'PROMPT'
You are a memory extractor. Analyze this agent execution and extract durable, reusable facts.

Extract ONLY facts that will remain useful across future executions:
- Capabilities: what this agent does well
- Constraints: limitations or boundaries observed
- Preferences: output format, tone, or style patterns
- Patterns: recurring behaviors or approaches
- Domain knowledge: specific knowledge demonstrated
- Tooling: which tools or APIs were used effectively

DO NOT extract task-specific details (e.g., "processed order #123").

Return ONLY valid JSON (no markdown fences):
{
  "facts": [
    {
      "fact": "concise, durable statement",
      "confidence": 0.85,
      "category": "knowledge",
      "belief_type": "preference",
      "preference_subtype": "style",
      "why_it_matters": "actionable directive: how a future run should behave because of this fact",
      "domain": "domain:code",
      "rejected_alternatives": [{"option": "approach that was ruled out", "reason": "why it was rejected"}],
      "tags": ["capability"]
    }
  ]
}

Confidence: 0.0 = speculative, 1.0 = clearly demonstrated.
Category must be exactly one of: preference, knowledge, context, behavior, goal
  - preference: user/team style choices, format preferences
  - knowledge: facts, domain knowledge, learned information
  - context: situational context, recent events, current state
  - behavior: working patterns, process preferences, habits
  - goal: objectives, targets, desired outcomes
belief_type must be exactly one of: preference, decision, entity, relation, open_question
  - preference: how the user/agent works and communicates
  - decision: a commitment future runs must respect
  - entity: a named thing in the user's world (a service, team, system)
  - relation: a connection between entities (a dependency, ownership)
  - open_question: something being worked through, not yet decided
preference_subtype is OPTIONAL and only valid when belief_type is "preference":
  - expertise: depth calibration — how much to explain
  - style: communication tone, format, voice
why_it_matters: a single actionable directive, not a restatement of the fact.
  Write "shapes all code examples toward TypeScript strict mode" — not "uses TypeScript".
domain: a scope tag so the belief only surfaces in matching sessions. Use
  "domain:code", "domain:writing", "domain:ops", etc., or "user:universal" when
  the fact applies everywhere. Omit if unsure.
rejected_alternatives is OPTIONAL: options the agent considered and explicitly
  ruled out. Capture these especially for "decision" beliefs so a future run
  does not re-propose a vetoed approach. Each entry: {"option": "...", "reason": "..."}.
  Omit entirely when nothing was ruled out.
Tags must be one or more of: capability, constraint, preference, pattern, domain, tooling
PROMPT;

    public function __construct(
        private readonly AiGatewayInterface $gateway,
        private readonly ProviderResolver $providerResolver,
        private readonly StoreMemoryAction $storeMemory,
    ) {}

    public function execute(string $agentId, string $teamId, string $executionId, ?string $userId = null): void
    {
        if (! config('memory.enabled', true)) {
            return;
        }

        $execution = AgentExecution::withoutGlobalScopes()
            ->where('id', $executionId)
            ->where('agent_id', $agentId)
            ->where('status', 'completed')
            ->first();

        if (! $execution) {
            return;
        }

        $agent = Agent::withoutGlobalScopes()->find($agentId);
        if (! $agent) {
            return;
        }

        // Derive userId from the execution's experiment if not passed explicitly.
        // Required for VPS-routed agents — LocalAgentGateway::executeVps loads the
        // user via User::find($request->userId) and the gate denies if it's null.
        if ($userId === null && $execution->experiment_id) {
            $userId = Experiment::withoutGlobalScopes()
                ->where('id', $execution->experiment_id)
                ->value('user_id');
        }

        try {
            $team = Team::find($teamId);
            $resolved = $this->providerResolver->resolve(agent: $agent, team: $team);

            // The model carries its own provider prefix ("anthropic/claude-haiku-4-5")
            // so the model name is never paired with a foreign resolved provider
            // (which 400s on gateways that don't expose Anthropic models). An
            // un-prefixed override falls back to the team-resolved provider.
            $configured = (string) config('memory.extraction.model', 'anthropic/claude-haiku-4-5');
            [$extractProvider, $extractModel] = str_contains($configured, '/')
                ? explode('/', $configured, 2)
                : [$resolved['provider'], $configured];

            $prompt = $this->buildExtractionPrompt($execution);

            $response = $this->gateway->complete(new AiRequestDTO(
                provider: $extractProvider,
                model: $extractModel,
                systemPrompt: self::SYSTEM_PROMPT,
                userPrompt: $prompt,
                maxTokens: 512,
                userId: $userId,
                teamId: $teamId,
                agentId: $agentId,
                experimentId: $execution->experiment_id,
                purpose: 'memory.extract',
                temperature: 0.1,
            ));

            // Some models wrap JSON in markdown fences (```json … ```) despite the
            // system prompt asking for raw JSON. Strip them before decode so we
            // don't lose the extracted facts on a cosmetic formatting choice.
            $content = trim($response->content);
            if (str_starts_with($content, '```')) {
                $content = preg_replace('/^```(?:json)?\s*\n?/', '', $content);
                $content = preg_replace('/\n?```\s*$/', '', $content);
            }

            $result = json_decode((string) $content, true);

            if (! is_array($result) || ! isset($result['facts']) || ! is_array($result['facts'])) {
                Log::warning('ExtractAndStoreMemoriesAction: invalid response format', [
                    'agent_id' => $agentId,
                    'execution_id' => $executionId,
                    'content' => substr($response->content, 0, 200),
                ]);

                return;
            }

            $stored = 0;
            foreach ($result['facts'] as $item) {
                $fact = trim($item['fact'] ?? '');
                $confidence = (float) ($item['confidence'] ?? 0.0);
                $tags = array_values(array_filter((array) ($item['tags'] ?? []), 'is_string'));
                $categoryValue = $item['category'] ?? null;
                $category = $categoryValue ? MemoryCategory::tryFrom($categoryValue) : null;

                $beliefType = isset($item['belief_type']) && is_string($item['belief_type'])
                    ? MemoryBeliefType::tryFrom($item['belief_type'])
                    : null;
                $preferenceSubtype = isset($item['preference_subtype']) && is_string($item['preference_subtype'])
                    ? MemoryPreferenceSubtype::tryFrom($item['preference_subtype'])
                    : null;
                $whyItMatters = isset($item['why_it_matters']) && is_string($item['why_it_matters'])
                    ? trim($item['why_it_matters'])
                    : null;
                $domain = isset($item['domain']) && is_string($item['domain']) && $item['domain'] !== ''
                    ? $item['domain']
                    : null;
                $rejectedAlternatives = isset($item['rejected_alternatives']) && is_array($item['rejected_alternatives'])
                    ? $item['rejected_alternatives']
                    : [];

                if ($fact === '' || $confidence < self::MIN_CONFIDENCE) {
                    continue;
                }

                $this->storeMemory->execute(
                    teamId: $teamId,
                    agentId: $agentId,
                    content: $fact,
                    sourceType: 'execution',
                    projectId: null,
                    sourceId: $executionId,
                    metadata: ['extracted_at' => now()->toIso8601String()],
                    confidence: $confidence,
                    tags: $tags,
                    tier: MemoryTier::Proposed,
                    proposedBy: "agent:{$agentId}",
                    category: $category,
                    beliefType: $beliefType,
                    preferenceSubtype: $preferenceSubtype,
                    whyItMatters: $whyItMatters !== '' ? $whyItMatters : null,
                    // Extracted facts are derived, not explicitly stated — they
                    // await confirmation before becoming active beliefs.
                    beliefStatus: MemoryBeliefStatus::Inferred,
                    domain: $domain,
                    rejectedAlternatives: $rejectedAlternatives,
                );

                $stored++;
            }

            Log::info('ExtractAndStoreMemoriesAction: extraction complete', [
                'agent_id' => $agentId,
                'execution_id' => $executionId,
                'facts_extracted' => count($result['facts']),
                'facts_stored' => $stored,
            ]);
        } catch (\Throwable $e) {
            Log::warning('ExtractAndStoreMemoriesAction: extraction failed, continuing', [
                'agent_id' => $agentId,
                'execution_id' => $executionId,
                'error' => $e->getMessage(),
            ]);
        }
    }

    private function buildExtractionPrompt(AgentExecution $execution): string
    {
        $parts = [];

        if (! empty($execution->input)) {
            $parts[] = '## Input'."\n".json_encode($execution->input, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        }

        if (! empty($execution->output)) {
            $outputText = json_encode($execution->output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
            // Limit output to 3000 chars to avoid excessive token use
            if (strlen($outputText) > 3000) {
                $outputText = substr($outputText, 0, 3000).'... [truncated]';
            }
            $parts[] = '## Output'."\n".$outputText;
        }

        if (! empty($execution->tools_used)) {
            $parts[] = '## Tools Used'."\n".json_encode($execution->tools_used, JSON_UNESCAPED_UNICODE);
        }

        return implode("\n\n", $parts);
    }
}
