<?php

namespace App\Domain\Memory\Actions;

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\Memory\Enums\MemoryVisibility;
use App\Domain\Memory\Enums\WriteGateDecision;
use App\Domain\Memory\Jobs\ClassifyMemoryTopicJob;
use App\Domain\Memory\Jobs\ContextualChunkEnricherJob;
use App\Domain\Memory\Models\Memory;
use App\Infrastructure\AI\Contracts\AiGatewayInterface;
use App\Infrastructure\AI\Contracts\EmbeddingProviderInterface;
use App\Infrastructure\AI\DTOs\AiRequestDTO;
use Illuminate\Support\Facades\Log;

class StoreMemoryAction
{
    private const MERGE_MODEL = 'claude-haiku-4-5';

    private const MERGE_PROMPT = <<<'PROMPT'
Merge these two related facts into one concise statement that preserves all unique details.
Drop redundancies. Keep specific details (dates, numbers, names).

Existing: %s
New: %s

Output: merged fact only, no preamble.
PROMPT;

    public function __construct(
        private readonly ?AiGatewayInterface $gateway = null,
    ) {}

    /**
     * Chunk content, generate embeddings, and store as Memory records.
     * Write gate deduplicates before inserting.
     *
     * @return Memory[]
     */
    public function execute(
        string $teamId,
        ?string $agentId,
        string $content,
        string $sourceType,
        ?string $projectId = null,
        ?string $sourceId = null,
        array $metadata = [],
        float $confidence = 1.0,
        float $importance = 0.5,
        array $tags = [],
        ?MemoryVisibility $visibility = null,
        MemoryTier $tier = MemoryTier::Working,
        ?string $proposedBy = null,
        ?MemoryCategory $category = null,
        ?string $topic = null,
        ?string $documentContext = null,
        ?MemoryBeliefType $beliefType = null,
        ?MemoryPreferenceSubtype $preferenceSubtype = null,
        ?string $whyItMatters = null,
        MemoryBeliefStatus $beliefStatus = MemoryBeliefStatus::Active,
        ?string $domain = null,
        array $rejectedAlternatives = [],
        ?string $supersedesId = null,
        ?string $taskType = null,
    ): array {
        if (! config('memory.enabled', true)) {
            return [];
        }

        if (empty(trim($content))) {
            return [];
        }

        // Determine visibility from source type if not explicitly set
        $visibility ??= $this->resolveVisibility($sourceType);

        $rejectedAlternatives = $this->normalizeRejectedAlternatives($rejectedAlternatives);

        $chunks = $this->chunkContent($content);
        $memories = [];

        foreach ($chunks as $chunk) {
            try {
                $memory = $this->storeChunk(
                    $teamId, $agentId, $chunk, $sourceType,
                    $projectId, $sourceId, $metadata, $confidence,
                    $importance, $tags, $visibility, $tier, $proposedBy, $category, $topic,
                    $beliefType, $preferenceSubtype, $whyItMatters, $beliefStatus, $domain,
                    $rejectedAlternatives, $supersedesId, $taskType,
                );

                if ($memory) {
                    $memories[] = $memory;

                    // Async topic classification when no topic was provided
                    if ($topic === null && $memory->topic === null) {
                        ClassifyMemoryTopicJob::dispatch($memory->id);
                    }

                    // Contextual RAG: enrich new chunks with LLM-generated situating context
                    if ($documentContext !== null && $memory->chunk_context === null) {
                        ContextualChunkEnricherJob::dispatch($memory->id, $documentContext);
                    }
                }
            } catch (\Throwable $e) {
                Log::warning('StoreMemoryAction: Failed to store memory chunk', [
                    'agent_id' => $agentId,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        // RoBrain supersession: when this memory replaces an older one, mark
        // the old belief Superseded so it stays queryable for audit but is
        // never injected into future agent context again.
        if ($supersedesId !== null && $memories !== []) {
            Memory::withoutGlobalScopes()
                ->where('id', $supersedesId)
                ->where('team_id', $teamId)
                ->update(['belief_status' => MemoryBeliefStatus::Superseded->value]);
        }

        return $memories;
    }

    /**
     * Coerce rejected-alternative entries to the {option, reason} shape and
     * drop any without a usable option. LLM extractors emit loose arrays;
     * this is the single funnel that cleans them.
     *
     * @param  array<int, mixed>  $raw
     * @return array<int, array{option: string, reason: string}>
     */
    private function normalizeRejectedAlternatives(array $raw): array
    {
        $clean = [];

        foreach ($raw as $entry) {
            if (! is_array($entry)) {
                continue;
            }

            $option = trim((string) ($entry['option'] ?? ''));
            if ($option === '') {
                continue;
            }

            $clean[] = [
                'option' => mb_substr($option, 0, 200),
                'reason' => mb_substr(trim((string) ($entry['reason'] ?? '')), 0, 500),
            ];
        }

        return $clean;
    }

    /**
     * Store a single chunk with write gate deduplication.
     */
    private function storeChunk(
        string $teamId,
        string $agentId,
        string $chunk,
        string $sourceType,
        ?string $projectId,
        ?string $sourceId,
        array $metadata,
        float $confidence,
        float $importance,
        array $tags,
        MemoryVisibility $visibility,
        MemoryTier $tier = MemoryTier::Working,
        ?string $proposedBy = null,
        ?MemoryCategory $category = null,
        ?string $topic = null,
        ?MemoryBeliefType $beliefType = null,
        ?MemoryPreferenceSubtype $preferenceSubtype = null,
        ?string $whyItMatters = null,
        MemoryBeliefStatus $beliefStatus = MemoryBeliefStatus::Active,
        ?string $domain = null,
        array $rejectedAlternatives = [],
        ?string $supersedesId = null,
        ?string $taskType = null,
    ): ?Memory {
        $contentHash = hash('sha256', mb_strtolower(trim($chunk)));
        $embedding = $this->generateEmbedding($chunk, $teamId);

        if ($embedding === null) {
            // No usable embedding key — skip storage. The platform is BYOK;
            // teams without an OpenAI/embedding credential simply won't have
            // semantic memory. Log at DEBUG to avoid noise.
            Log::debug('StoreMemoryAction: skipping chunk — no embedding key configured', [
                'team_id' => $teamId,
                'agent_id' => $agentId,
            ]);

            return null;
        }

        // Write gate: check for duplicates before inserting
        $decision = $this->evaluateWriteGate($teamId, $agentId, $contentHash, $embedding);

        return match ($decision->decision) {
            WriteGateDecision::Skip => $this->handleSkip($decision->existingMemory),
            WriteGateDecision::Update => $this->handleUpdate(
                $decision->existingMemory, $chunk, $embedding,
                $teamId, $agentId, $confidence, $importance, $tags,
            ),
            WriteGateDecision::Add => $this->handleAdd(
                $teamId, $agentId, $chunk, $embedding, $contentHash,
                $sourceType, $projectId, $sourceId, $metadata,
                $confidence, $importance, $tags, $visibility, $tier, $proposedBy, $category, $topic,
                $beliefType, $preferenceSubtype, $whyItMatters, $beliefStatus, $domain,
                $rejectedAlternatives, $supersedesId, $taskType,
            ),
        };
    }

    /**
     * Two-stage write gate: hash check → semantic similarity.
     */
    private function evaluateWriteGate(
        string $teamId,
        string $agentId,
        string $contentHash,
        string $embedding,
    ): WriteGateResult {
        if (! config('memory.write_gate.enabled', true)) {
            return new WriteGateResult(WriteGateDecision::Add);
        }

        // Stage 1: Hash dedup (exact match)
        if (config('memory.write_gate.hash_dedup', true)) {
            $existing = Memory::withoutGlobalScopes()
                ->where('agent_id', $agentId)
                ->where('team_id', $teamId)
                ->where('content_hash', $contentHash)
                ->first();

            if ($existing) {
                Log::debug('WriteGate: SKIP (hash match)', ['memory_id' => $existing->id]);

                return new WriteGateResult(WriteGateDecision::Skip, $existing);
            }
        }

        // Stage 2: Semantic similarity (pgvector ANN)
        $skipThreshold = config('memory.write_gate.skip_threshold', 0.95);
        $updateThreshold = config('memory.write_gate.update_threshold', 0.85);

        try {
            $neighbor = Memory::withoutGlobalScopes()
                ->where('agent_id', $agentId)
                ->where('team_id', $teamId)
                ->whereNotNull('embedding')
                ->selectRaw('*, 1 - (embedding <=> ?) AS similarity', [$embedding])
                ->orderByRaw('embedding <=> ?', [$embedding])
                ->limit(1)
                ->first();

            if ($neighbor && $neighbor->similarity >= $skipThreshold) {
                Log::debug('WriteGate: SKIP (semantic)', [
                    'memory_id' => $neighbor->id,
                    'similarity' => round($neighbor->similarity, 4),
                ]);

                return new WriteGateResult(WriteGateDecision::Skip, $neighbor);
            }

            if ($neighbor && $neighbor->similarity >= $updateThreshold) {
                Log::debug('WriteGate: UPDATE (semantic merge)', [
                    'memory_id' => $neighbor->id,
                    'similarity' => round($neighbor->similarity, 4),
                ]);

                return new WriteGateResult(WriteGateDecision::Update, $neighbor);
            }
        } catch (\Throwable $e) {
            // pgvector not available (e.g., SQLite tests) — fall through to ADD
            Log::debug('WriteGate: semantic check failed, falling through to ADD', [
                'error' => $e->getMessage(),
            ]);
        }

        return new WriteGateResult(WriteGateDecision::Add);
    }

    private function handleSkip(?Memory $existing): ?Memory
    {
        // Touch last_accessed_at to signal the duplicate was seen again
        if ($existing) {
            $existing->update(['last_accessed_at' => now()]);
        }

        return $existing;
    }

    private function handleUpdate(
        ?Memory $existing,
        string $newContent,
        string $newEmbedding,
        string $teamId,
        string $agentId,
        float $newConfidence,
        float $newImportance,
        array $newTags,
    ): ?Memory {
        if (! $existing) {
            return null;
        }

        // Try LLM-assisted merge if gateway is available
        $mergedContent = $this->mergeContent($existing->content, $newContent, $teamId, $agentId);

        if ($mergedContent && $mergedContent !== $existing->content) {
            $mergedEmbedding = $this->generateEmbedding($mergedContent, $teamId);
            if ($mergedEmbedding === null) {
                return null;
            }
            $mergedHash = hash('sha256', mb_strtolower(trim($mergedContent)));

            $existing->update([
                'content' => $mergedContent,
                'embedding' => $mergedEmbedding,
                'content_hash' => $mergedHash,
                'confidence' => max($existing->confidence, $newConfidence),
                'importance' => max($existing->importance ?? 0.5, $newImportance),
                'tags' => array_values(array_unique(array_merge($existing->tags ?? [], $newTags))),
                'last_accessed_at' => now(),
            ]);
        } else {
            // Fallback: just bump scores without changing content
            $existing->update([
                'confidence' => max($existing->confidence, $newConfidence),
                'importance' => max($existing->importance ?? 0.5, $newImportance),
                'tags' => array_values(array_unique(array_merge($existing->tags ?? [], $newTags))),
                'last_accessed_at' => now(),
            ]);
        }

        return $existing->fresh();
    }

    private function handleAdd(
        string $teamId,
        string $agentId,
        string $chunk,
        string $embedding,
        string $contentHash,
        string $sourceType,
        ?string $projectId,
        ?string $sourceId,
        array $metadata,
        float $confidence,
        float $importance,
        array $tags,
        MemoryVisibility $visibility,
        MemoryTier $tier = MemoryTier::Working,
        ?string $proposedBy = null,
        ?MemoryCategory $category = null,
        ?string $topic = null,
        ?MemoryBeliefType $beliefType = null,
        ?MemoryPreferenceSubtype $preferenceSubtype = null,
        ?string $whyItMatters = null,
        MemoryBeliefStatus $beliefStatus = MemoryBeliefStatus::Active,
        ?string $domain = null,
        array $rejectedAlternatives = [],
        ?string $supersedesId = null,
        ?string $taskType = null,
    ): Memory {
        // A preference subtype only applies to Preference beliefs — drop it otherwise.
        if ($beliefType?->acceptsPreferenceSubtype() !== true) {
            $preferenceSubtype = null;
        }

        return Memory::create([
            'team_id' => $teamId,
            'agent_id' => $agentId,
            'project_id' => $projectId,
            'content' => $chunk,
            'embedding' => $embedding,
            'content_hash' => $contentHash,
            'metadata' => $metadata,
            'source_type' => $sourceType,
            'source_id' => $sourceId,
            'confidence' => $confidence,
            'importance' => $importance,
            'tags' => $tags,
            'visibility' => $visibility,
            'tier' => $tier,
            'category' => $category,
            'topic' => $topic,
            'proposed_by' => $proposedBy,
            'belief_type' => $beliefType,
            'preference_subtype' => $preferenceSubtype,
            'why_it_matters' => $whyItMatters,
            'belief_status' => $beliefStatus,
            'domain' => $domain,
            'task_type' => $taskType,
            'rejected_alternatives' => $rejectedAlternatives,
            'supersedes_id' => $supersedesId,
        ]);
    }

    /**
     * LLM-assisted merge of two related facts.
     */
    private function mergeContent(string $existing, string $new, string $teamId, string $agentId): ?string
    {
        if (! $this->gateway) {
            return null;
        }

        try {
            $response = $this->gateway->complete(new AiRequestDTO(
                provider: 'anthropic',
                model: self::MERGE_MODEL,
                systemPrompt: 'You are a memory consolidator. Output only the merged fact.',
                userPrompt: sprintf(self::MERGE_PROMPT, $existing, $new),
                maxTokens: 256,
                teamId: $teamId,
                agentId: $agentId,
                purpose: 'memory.merge',
                temperature: 0.1,
            ));

            $merged = trim($response->content);

            return $merged !== '' ? $merged : null;
        } catch (\Throwable $e) {
            Log::debug('WriteGate: merge failed, keeping existing content', [
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * Determine default visibility from source type.
     */
    private function resolveVisibility(string $sourceType): MemoryVisibility
    {
        return match ($sourceType) {
            'document', 'manual_upload' => MemoryVisibility::Team,
            'experiment' => MemoryVisibility::Project,
            default => MemoryVisibility::Private,
        };
    }

    /**
     * Split content into chunks of configurable max size.
     *
     * @return string[]
     */
    private function chunkContent(string $content): array
    {
        $maxSize = config('memory.max_chunk_size', 2000);

        if (strlen($content) <= $maxSize) {
            return [$content];
        }

        $chunks = [];
        $paragraphs = preg_split('/\n\n+/', $content);
        $currentChunk = '';

        foreach ($paragraphs as $paragraph) {
            if (strlen($currentChunk) + strlen($paragraph) + 2 > $maxSize) {
                if ($currentChunk !== '') {
                    $chunks[] = trim($currentChunk);
                }
                if (strlen($paragraph) > $maxSize) {
                    $sentences = preg_split('/(?<=[.!?])\s+/', $paragraph);
                    $currentChunk = '';
                    foreach ($sentences as $sentence) {
                        if (strlen($currentChunk) + strlen($sentence) + 1 > $maxSize) {
                            if ($currentChunk !== '') {
                                $chunks[] = trim($currentChunk);
                            }
                            $currentChunk = $sentence;
                        } else {
                            $currentChunk .= ($currentChunk ? ' ' : '').$sentence;
                        }
                    }
                } else {
                    $currentChunk = $paragraph;
                }
            } else {
                $currentChunk .= ($currentChunk ? "\n\n" : '').$paragraph;
            }
        }

        if (trim($currentChunk) !== '') {
            $chunks[] = trim($currentChunk);
        }

        return $chunks;
    }

    /**
     * Generate embedding vector using PrismPHP, BYOK-aware.
     *
     * @return string|null Vector string for pgvector (e.g. "[0.1,0.2,...]"),
     *                     or null when no embedding key is reachable for this
     *                     team. Caller is expected to skip storage gracefully.
     */
    private function generateEmbedding(string $text, ?string $teamId): ?string
    {
        $service = app(EmbeddingProviderInterface::class);

        $vector = $service->embedForTeam($text, $teamId);

        return $vector === null ? null : $service->formatForPgvector($vector);
    }
}

/**
 * Internal value object for write gate evaluation result.
 */
class WriteGateResult
{
    public function __construct(
        public readonly WriteGateDecision $decision,
        public readonly ?Memory $existingMemory = null,
    ) {}
}
