<?php

namespace App\Domain\Memory\Actions;

use App\Domain\Experiment\Models\Experiment;
use App\Domain\Memory\Enums\MemoryTier;
use App\Infrastructure\AI\Contracts\AiGatewayInterface;
use App\Infrastructure\AI\DTOs\AiRequestDTO;
use Illuminate\Support\Facades\Log;

/**
 * Extracts a failure lesson from a failed Experiment using the configured
 * memory-extraction model (config('memory.extraction.model')).
 *
 * Stores the lesson as a Memory record with tier = MemoryTier::Failures so it
 * receives the curated retrieval boost and surfaces prominently in future executions
 * for the same agent.
 */
class ExtractFailureLessonAction
{
    private const SYSTEM_PROMPT = <<<'PROMPT'
You are a failure analyst. Analyze this failed experiment and extract a single, concise failure lesson.

The lesson must be:
- Actionable: describes what to do differently next time
- Durable: useful beyond this specific experiment
- Specific: names the root cause (e.g., tool timeout, missing context, ambiguous goal)

Return ONLY valid JSON (no markdown fences):
{
  "lesson": "concise failure lesson in one or two sentences",
  "root_cause": "brief root cause label",
  "confidence": 0.85,
  "tags": ["constraint"]
}

Confidence: 0.0 = speculative, 1.0 = clearly demonstrated.
Tags must be one or more of: capability, constraint, preference, pattern, domain, tooling
PROMPT;

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

    public function execute(string $experimentId, string $teamId): void
    {
        if (! config('memory.enabled', true)) {
            return;
        }

        $experiment = Experiment::withoutGlobalScopes()
            ->where('id', $experimentId)
            ->where('team_id', $teamId)
            ->first();

        if (! $experiment) {
            return;
        }

        $agentId = $experiment->agent_id;
        if (! $agentId) {
            return;
        }

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

            $response = $this->gateway->complete(new AiRequestDTO(
                provider: $provider,
                model: $model,
                systemPrompt: self::SYSTEM_PROMPT,
                userPrompt: $this->buildPrompt($experiment),
                maxTokens: 256,
                teamId: $teamId,
                experimentId: $experimentId,
                purpose: 'memory.failure_lesson',
                temperature: 0.1,
            ));

            $result = json_decode($response->content, true);

            if (! is_array($result) || empty($result['lesson'])) {
                Log::warning('ExtractFailureLessonAction: invalid response', [
                    'experiment_id' => $experimentId,
                    'content' => substr($response->content, 0, 200),
                ]);

                return;
            }

            $lesson = trim($result['lesson']);
            $confidence = (float) ($result['confidence'] ?? 0.5);
            $tags = array_values(array_filter((array) ($result['tags'] ?? []), 'is_string'));
            $rootCause = trim($result['root_cause'] ?? '');

            if ($confidence < 0.3) {
                return;
            }

            $proposalWorkflow = (bool) config('memory.proposal_workflow.extractors_enabled', false);
            $taskType = $this->inferTaskType($experiment);

            $metadata = [
                'experiment_id' => $experimentId,
                'experiment_title' => $experiment->title,
                'final_status' => $experiment->status?->value,
                'root_cause' => $rootCause,
                'task_type' => $taskType,
                'extracted_at' => now()->toIso8601String(),
            ];

            if ($proposalWorkflow) {
                $metadata['target_tier'] = MemoryTier::Failures->value;
            }

            // When proposal_workflow is off (default), write directly to the
            // Failures tier so the curated (+0.10) retrieval boost applies
            // immediately. When on, the lesson routes through Proposed and the
            // heuristic auditor decides whether to promote.
            $this->storeMemory->execute(
                teamId: $teamId,
                agentId: $agentId,
                content: $lesson,
                sourceType: 'experiment',
                projectId: null,
                sourceId: $experimentId,
                metadata: $metadata,
                confidence: $confidence,
                tags: $tags,
                tier: $proposalWorkflow ? MemoryTier::Proposed : MemoryTier::Failures,
                proposedBy: 'system:failure_extractor',
                taskType: $taskType,
            );

            Log::info('ExtractFailureLessonAction: lesson stored', [
                'experiment_id' => $experimentId,
                'agent_id' => $agentId,
                'root_cause' => $rootCause,
            ]);
        } catch (\Throwable $e) {
            Log::warning('ExtractFailureLessonAction: extraction failed, continuing', [
                'experiment_id' => $experimentId,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Derive a reusable task_type for episodic recall. Prefers the experiment's
     * track (a stable taxonomy: growth/retention/debug/workflow/…); falls back
     * to null when the experiment has no track. Deterministic — no LLM call.
     */
    private function inferTaskType(Experiment $experiment): ?string
    {
        return $experiment->track?->value;
    }

    private function buildPrompt(Experiment $experiment): string
    {
        // User-controlled fields are wrapped in XML-style delimiters to prevent prompt injection.
        $parts = [
            '## Experiment',
            '<experiment_title>'.htmlspecialchars((string) $experiment->title, ENT_XML1).'</experiment_title>',
        ];

        if (! empty($experiment->thesis)) {
            $parts[] = '<experiment_thesis>'.htmlspecialchars((string) $experiment->thesis, ENT_XML1).'</experiment_thesis>';
        }

        $parts[] = 'Final status: '.($experiment->status?->value ?? 'unknown');

        $failedStages = $experiment->stages()
            ->whereNotNull('output_snapshot->error')
            ->orderByDesc('created_at')
            ->limit(3)
            ->get(['stage', 'output_snapshot']);

        if ($failedStages->isNotEmpty()) {
            $parts[] = "\n## Failed Stages";
            foreach ($failedStages as $stage) {
                $parts[] = 'Stage: '.$stage->stage->value;
                $error = $stage->output_snapshot['error'] ?? null;
                if (! empty($error)) {
                    $errorText = is_array($error)
                        ? json_encode($error, JSON_UNESCAPED_UNICODE)
                        : (string) $error;
                    $parts[] = '<stage_error>'.htmlspecialchars(substr($errorText, 0, 500), ENT_XML1).'</stage_error>';
                }
            }
        }

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