<?php

namespace App\Domain\Skill\Actions;

use App\Domain\Agent\Models\Agent;
use App\Domain\Budget\Actions\ReserveBudgetAction;
use App\Domain\Budget\Actions\SettleBudgetAction;
use App\Domain\Marketplace\Actions\RecordMarketplaceUsageAction;
use App\Domain\Shared\Models\Team;
use App\Domain\Skill\Enums\SkillType;
use App\Domain\Skill\Events\SkillExecuted;
use App\Domain\Skill\Events\SkillExecuting;
use App\Domain\Skill\Exceptions\SkillProviderIncompatibleException;
use App\Domain\Skill\Models\Skill;
use App\Domain\Skill\Models\SkillExecution;
use App\Domain\Skill\Services\MultiModelConsensusService;
use App\Domain\Skill\Services\SchemaValidator;
use App\Domain\Skill\Services\SkillCompatibilityChecker;
use App\Domain\Skill\Services\SkillCostCalculator;
use App\Infrastructure\AI\Contracts\AiGatewayInterface;
use App\Infrastructure\AI\DTOs\AiRequestDTO;
use App\Infrastructure\AI\DTOs\AiResponseDTO;
use App\Infrastructure\AI\Services\ProviderResolver;
use Illuminate\Support\Facades\Log;

class ExecuteSkillAction
{
    public function __construct(
        private readonly AiGatewayInterface $gateway,
        private readonly SchemaValidator $schemaValidator,
        private readonly SkillCostCalculator $costCalculator,
        private readonly ReserveBudgetAction $reserveBudget,
        private readonly SettleBudgetAction $settleBudget,
        private readonly ProviderResolver $providerResolver,
        private readonly SkillCompatibilityChecker $compatibilityChecker,
        private readonly RecordMarketplaceUsageAction $recordMarketplaceUsage,
        private readonly MultiModelConsensusService $consensusService,
        private readonly ExecuteCodeExecutionSkillAction $executeCodeExecution,
        private readonly ExecuteBrowserSkillAction $executeBrowserSkill,
        private readonly ExecuteRunPodSkillAction $executeRunPod,
        private readonly ExecuteRunPodPodSkillAction $executeRunPodPod,
        private readonly ExecuteGpuComputeSkillAction $executeGpuCompute,
        private readonly ExecuteBorunaScriptSkillAction $executeBorunaScript,
        private readonly ExecuteSupabaseEdgeFunctionSkillAction $executeSupabaseEdgeFunction,
    ) {}

    /**
     * Execute a skill following the full pipeline:
     * validate → reserve budget → call AI → validate output → record → settle budget
     *
     * @param  array  $input  The input data matching skill.input_schema
     * @param  string|null  $provider  Override provider (agent → team default → platform)
     * @param  string|null  $model  Override model
     * @return array{execution: SkillExecution, output: array|string|null}
     */
    public function execute(
        Skill $skill,
        array $input,
        string $teamId,
        string $userId,
        ?string $agentId = null,
        ?string $experimentId = null,
        ?string $provider = null,
        ?string $model = null,
        string $purpose = 'run',
    ): array {
        // CodeExecution has its own full pipeline (worktree + Docker sandbox + approval)
        if ($skill->type === SkillType::CodeExecution) {
            return $this->executeCodeExecution->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // Browser Automation calls Browserless REST API directly — no LLM, no budget reservation
        if ($skill->type === SkillType::Browser) {
            return $this->executeBrowserSkill->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // RunPod Endpoint calls RunPod REST API directly — no LLM, costs billed to user's RunPod account
        if ($skill->type === SkillType::RunpodEndpoint) {
            return $this->executeRunPod->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // RunPod Pod manages a full GPU pod lifecycle — create → wait → call → stop
        if ($skill->type === SkillType::RunpodPod) {
            return $this->executeRunPodPod->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // GpuCompute routes to the pluggable compute provider system
        if ($skill->type === SkillType::GpuCompute) {
            return $this->executeGpuCompute->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // BorunaScript runs a deterministic .ax script via the Boruna MCP stdio server — no LLM, no credits
        if ($skill->type === SkillType::BorunaScript) {
            return $this->executeBorunaScript->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // Supabase Edge Function calls a Supabase Edge Function via REST — no LLM, costs billed to user's Supabase account
        if ($skill->type === SkillType::SupabaseEdgeFunction) {
            return $this->executeSupabaseEdgeFunction->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
        }

        // Plugin hook: allow plugins to inspect input or cancel skill execution
        $executing = new SkillExecuting($skill, $input);
        event($executing);
        if ($executing->cancel) {
            return $this->failExecution($skill, $teamId, $agentId, $experimentId, $input, $executing->cancelReason ?? 'Cancelled by plugin');
        }
        $input = $executing->input;

        // 0. Check provider compatibility (if requirements declared)
        if (! empty($skill->provider_requirements)) {
            $team = Team::withoutGlobalScopes()->find($teamId);
            if ($team) {
                try {
                    $this->compatibilityChecker->assertCompatible($skill, $team);
                } catch (SkillProviderIncompatibleException $e) {
                    return $this->failExecution($skill, $teamId, $agentId, $experimentId, $input, $e->getMessage());
                }
            }
        }

        // 1. Validate input against schema
        if (! empty($skill->input_schema)) {
            $validation = $this->schemaValidator->validate($input, $skill->input_schema);

            if (! $validation['valid']) {
                return $this->failExecution($skill, $teamId, $agentId, $experimentId, $input,
                    'Input validation failed: '.implode('; ', $validation['errors']),
                );
            }
        }

        // 2. Resolve provider and model via ProviderResolver hierarchy
        if ($provider && $model) {
            $resolvedProvider = $provider;
            $resolvedModel = $model;
        } else {
            $agent = $agentId ? Agent::find($agentId) : null;
            $team = $teamId ? Team::find($teamId) : null;
            $resolved = $this->providerResolver->resolve($skill, $agent, $team, $purpose);
            $resolvedProvider = $provider ?? $resolved['provider'];
            $resolvedModel = $model ?? $resolved['model'];
        }

        // 3. Estimate and reserve budget
        $estimatedCost = $this->costCalculator->estimate($skill, $resolvedProvider, $resolvedModel);
        $reservation = $this->reserveBudget->execute(
            userId: $userId,
            teamId: $teamId,
            amount: $estimatedCost,
            experimentId: $experimentId,
            description: "Skill execution: {$skill->name}",
        );

        $startTime = hrtime(true);

        try {
            // 4. Execute based on skill type
            $response = $this->executeByType($skill, $input, $resolvedProvider, $resolvedModel, $teamId, $userId, $agentId, $experimentId);

            $durationMs = (int) ((hrtime(true) - $startTime) / 1_000_000);

            // 5. Validate output if schema defined — with retry-on-failure
            //    for LLM-backed skill types (Sprint 14, mirrors Agent Sprint 12).
            $output = $response->parsedOutput ?? json_decode($response->content, true);
            $schemaValid = true;
            $schemaRetryAttempts = 0;
            $schemaRetryTrail = [];

            if (! empty($skill->output_schema) && is_array($output)) {
                $outputValidation = $this->schemaValidator->validate($output, $skill->output_schema);
                $schemaValid = $outputValidation['valid'];

                if (! $schemaValid && $this->skillSupportsLlmRetry($skill)) {
                    $maxRetries = $this->resolveMaxRetries($skill);
                    while (! $schemaValid && $schemaRetryAttempts < $maxRetries) {
                        $schemaRetryAttempts++;
                        $schemaRetryTrail[] = [
                            'attempt' => $schemaRetryAttempts,
                            'errors' => array_slice($outputValidation['errors'], 0, 5),
                        ];

                        try {
                            $retryResponse = $this->executeLlmRetry(
                                skill: $skill,
                                input: $input,
                                provider: $resolvedProvider,
                                model: $resolvedModel,
                                teamId: $teamId,
                                userId: $userId,
                                agentId: $agentId,
                                experimentId: $experimentId,
                                previousOutput: (string) $response->content,
                                errors: $outputValidation['errors'],
                            );
                        } catch (\Throwable $e) {
                            Log::warning('ExecuteSkillAction: schema retry failed', [
                                'skill_id' => $skill->id,
                                'attempt' => $schemaRetryAttempts,
                                'error' => $e->getMessage(),
                            ]);
                            break;
                        }

                        $response = $retryResponse;
                        $output = $response->parsedOutput ?? json_decode($response->content, true);
                        if (! is_array($output)) {
                            break;
                        }
                        $outputValidation = $this->schemaValidator->validate($output, $skill->output_schema);
                        $schemaValid = $outputValidation['valid'];
                    }
                    $durationMs = (int) ((hrtime(true) - $startTime) / 1_000_000);
                }
            }

            // 6. Create execution record
            $executionData = [
                'skill_id' => $skill->id,
                'agent_id' => $agentId,
                'experiment_id' => $experimentId,
                'team_id' => $teamId,
                'status' => 'completed',
                'input' => $input,
                'output' => $output,
                'duration_ms' => $durationMs,
                'cost_credits' => $response->usage->costCredits,
            ];

            if ($skill->type === SkillType::MultiModelConsensus && is_array($output)) {
                $executionData['confidence_score'] = $output['confidence_score'] ?? null;
                $executionData['consensus_level'] = $output['consensus_level'] ?? null;
                $executionData['peer_reviews'] = $output['peer_reviews'] ?? null;
                $executionData['evaluation_method'] = 'multi_model_consensus';
                $config = is_array($skill->configuration) ? $skill->configuration : [];
                $executionData['judge_model'] = ($config['judge_model']['provider'] ?? 'anthropic')
                    .'/'
                    .($config['judge_model']['model'] ?? 'claude-sonnet-4-5');
                // Store only the synthesized answer in output, not the metadata columns
                $executionData['output'] = [
                    'answer' => $output['answer'] ?? $response->content,
                    'dissenting_view' => $output['dissenting_view'] ?? null,
                ];
            }

            // Record schema retry trail so operators can see which outputs
            // required self-correction (Sprint 14, mirrors Agent Sprint 12).
            if ($schemaRetryAttempts > 0) {
                $executionData['quality_details'] = [
                    'output_schema_valid' => $schemaValid,
                    'output_schema_retries' => $schemaRetryAttempts,
                    'output_schema_retry_trail' => $schemaRetryTrail,
                ];
            }

            $execution = SkillExecution::create($executionData);

            // 7. Update skill stats
            $skill->recordExecution(true, $durationMs);

            // 7a. Increment quality counters
            $skill->increment('completed_count');
            if ($execution->quality_score !== null && $execution->quality_score >= config('skills.degradation.quality_threshold', 0.5)) {
                $skill->increment('effective_count');
            }

            // 8. Settle budget
            $this->settleBudget->execute($reservation, $response->usage->costCredits);

            // 9. Record marketplace usage (no-op if not from marketplace)
            if ($skill->source_listing_id) {
                $this->recordMarketplaceUsage->execute($execution);
            }

            // Plugin hook: notify plugins of successful skill execution
            event(new SkillExecuted($skill, $execution, true));

            return [
                'execution' => $execution,
                'output' => $output,
            ];
        } catch (\Throwable $e) {
            $durationMs = (int) ((hrtime(true) - $startTime) / 1_000_000);

            // Settle budget with zero actual cost on failure
            $this->settleBudget->execute($reservation, 0);

            // Record failed execution
            $skill->recordExecution(false, $durationMs);

            $errorMessage = $e->getMessage() ?: 'Unknown error during skill execution ('.get_class($e).')';
            $failResult = $this->failExecution($skill, $teamId, $agentId, $experimentId, $input, $errorMessage, $durationMs);

            // Plugin hook: notify plugins of failed skill execution
            event(new SkillExecuted($skill, $failResult['execution'], false));

            // Record failed marketplace usage
            if ($skill->source_listing_id) {
                $this->recordMarketplaceUsage->execute($failResult['execution']);
            }

            return $failResult;
        }
    }

    /**
     * Only LLM-backed skill types benefit from retry-on-schema-failure.
     * Connector/Rule/CodeExecution/Browser/etc. don't produce LLM output
     * so retrying them wouldn't change the validation outcome.
     */
    private function skillSupportsLlmRetry(Skill $skill): bool
    {
        return in_array($skill->type, [
            SkillType::Llm,
            SkillType::Hybrid,
            SkillType::Guardrail,
        ], true);
    }

    /**
     * Resolve max-retries: per-skill override → config default → 0-5 clamp.
     * Reuses the same `agent.output_schema.max_retries_default` config key
     * from Sprint 12 so operators tune one knob for both Agents and Skills.
     */
    private function resolveMaxRetries(Skill $skill): int
    {
        $value = $skill->output_schema_max_retries;
        if ($value === null) {
            $value = (int) config('agent.output_schema.max_retries_default', 2);
        }

        return max(0, min(5, (int) $value));
    }

    /**
     * Re-run the LLM skill with a correction hint + the JSON Schema the
     * output must match. Temperature is lowered to 0.1 for determinism.
     *
     * @param  list<string>  $errors
     */
    private function executeLlmRetry(
        Skill $skill,
        array $input,
        string $provider,
        string $model,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
        string $previousOutput,
        array $errors,
    ): AiResponseDTO {
        $errorLines = implode("\n- ", $errors);
        $schemaJson = json_encode($skill->output_schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

        $systemPrompt = $skill->system_prompt ?? 'You are a helpful assistant.';
        $userPrompt = $this->buildUserPrompt($skill, $input);
        $retryHint = <<<PROMPT


        --- Retry ---
        Your previous response failed output-schema validation.

        Previous response (first 500 chars):
        {$this->truncate($previousOutput, 500)}

        Schema errors:
        - {$errorLines}

        Return a JSON object that matches this schema exactly. No prose, no
        code fences — just the JSON:

        ```json
        {$schemaJson}
        ```
        PROMPT;

        $request = new AiRequestDTO(
            provider: $provider,
            model: $model,
            systemPrompt: $systemPrompt,
            userPrompt: $userPrompt.$retryHint,
            maxTokens: $skill->configuration['max_tokens'] ?? 4096,
            outputSchema: null,
            userId: $userId,
            teamId: $teamId,
            experimentId: $experimentId,
            agentId: $agentId,
            purpose: "skill:{$skill->slug}:schema_retry",
            temperature: 0.1,
        );

        return $this->gateway->complete($request);
    }

    private function truncate(string $s, int $len): string
    {
        return mb_strimwidth($s, 0, $len, '…');
    }

    private function executeByType(
        Skill $skill,
        array $input,
        string $provider,
        string $model,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
    ): AiResponseDTO {
        return match ($skill->type) {
            SkillType::Llm, SkillType::Hybrid => $this->executeLlmSkill($skill, $input, $provider, $model, $teamId, $userId, $agentId, $experimentId),
            SkillType::Connector => $this->executeConnectorSkill($skill, $input, $provider, $model, $teamId, $userId, $agentId, $experimentId),
            SkillType::Rule => $this->executeRuleSkill($skill, $input, $provider, $model, $teamId, $userId, $agentId, $experimentId),
            SkillType::Guardrail => $this->executeLlmSkill($skill, $input, $provider, $model, $teamId, $userId, $agentId, $experimentId),
            SkillType::MultiModelConsensus => $this->executeMultiModelConsensusSkill($skill, $input, $teamId, $userId, $agentId, $experimentId),
            SkillType::CodeExecution, SkillType::Browser, SkillType::RunpodEndpoint, SkillType::RunpodPod, SkillType::GpuCompute, SkillType::BorunaScript, SkillType::SupabaseEdgeFunction, SkillType::RagflowRetrieval => throw new \LogicException('CodeExecution, Browser, RunpodEndpoint, RunpodPod, GpuCompute, BorunaScript, SupabaseEdgeFunction, and RagflowRetrieval skill types must be short-circuited before reaching executeByType.'),
        };
    }

    private function executeLlmSkill(
        Skill $skill,
        array $input,
        string $provider,
        string $model,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
    ): AiResponseDTO {
        $systemPrompt = $skill->system_prompt ?? 'You are a helpful assistant.';
        $userPrompt = $this->buildUserPrompt($skill, $input);

        $request = new AiRequestDTO(
            provider: $provider,
            model: $model,
            systemPrompt: $systemPrompt,
            userPrompt: $userPrompt,
            maxTokens: $skill->configuration['max_tokens'] ?? 4096,
            outputSchema: null, // Structured output via PrismPHP schemas handled at gateway level
            userId: $userId,
            teamId: $teamId,
            experimentId: $experimentId,
            agentId: $agentId,
            purpose: "skill:{$skill->slug}",
            temperature: $skill->configuration['temperature'] ?? 0.7,
        );

        return $this->gateway->complete($request);
    }

    private function executeConnectorSkill(
        Skill $skill,
        array $input,
        string $provider,
        string $model,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
    ): AiResponseDTO {
        // Connector skills wrap an LLM call that interprets the connector output
        // The connector config specifies which connector to invoke
        $connectorResult = json_encode($input);

        $systemPrompt = $skill->system_prompt ?? 'Process the following connector output and produce a structured result.';
        $userPrompt = "Connector output:\n{$connectorResult}\n\nTask: ".($skill->configuration['task'] ?? 'Summarize the data.');

        $request = new AiRequestDTO(
            provider: $provider,
            model: $model,
            systemPrompt: $systemPrompt,
            userPrompt: $userPrompt,
            maxTokens: $skill->configuration['max_tokens'] ?? 2048,
            userId: $userId,
            teamId: $teamId,
            experimentId: $experimentId,
            agentId: $agentId,
            purpose: "skill:{$skill->slug}:connector",
            temperature: $skill->configuration['temperature'] ?? 0.3,
        );

        return $this->gateway->complete($request);
    }

    private function executeRuleSkill(
        Skill $skill,
        array $input,
        string $provider,
        string $model,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
    ): AiResponseDTO {
        // Rule skills use LLM to evaluate rules against input
        $rules = json_encode($skill->configuration['rules'] ?? []);

        $systemPrompt = $skill->system_prompt ?? 'Evaluate the following rules against the input and return a JSON result.';
        $userPrompt = "Rules:\n{$rules}\n\nInput:\n".json_encode($input)."\n\nEvaluate each rule and return the results.";

        $request = new AiRequestDTO(
            provider: $provider,
            model: $model,
            systemPrompt: $systemPrompt,
            userPrompt: $userPrompt,
            maxTokens: $skill->configuration['max_tokens'] ?? 2048,
            userId: $userId,
            teamId: $teamId,
            experimentId: $experimentId,
            agentId: $agentId,
            purpose: "skill:{$skill->slug}:rule",
            temperature: 0.1,
        );

        return $this->gateway->complete($request);
    }

    private function executeMultiModelConsensusSkill(
        Skill $skill,
        array $input,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
    ): AiResponseDTO {
        $config = is_array($skill->configuration) ? $skill->configuration : [];
        $models = $config['models'] ?? [
            ['provider' => 'anthropic', 'model' => 'claude-sonnet-4-5'],
            ['provider' => 'openai', 'model' => 'gpt-4o'],
            ['provider' => 'google', 'model' => 'gemini-2.5-flash'],
        ];
        $judgeModel = $config['judge_model'] ?? ['provider' => 'anthropic', 'model' => 'claude-sonnet-4-5'];

        $result = $this->consensusService->run(
            prompt: $this->buildUserPrompt($skill, $input),
            systemPrompt: $skill->system_prompt ?? '',
            models: $models,
            judgeModel: $judgeModel,
            teamId: $teamId,
            userId: $userId,
            experimentId: $experimentId,
            agentId: $agentId,
        );

        /** @var AiResponseDTO $response */
        $response = $result['response'];

        // Enrich parsedOutput with peer_reviews so the execution record can persist it
        return new AiResponseDTO(
            content: $response->content,
            parsedOutput: array_merge($response->parsedOutput ?? [], [
                'peer_reviews' => $result['peer_reviews'],
            ]),
            usage: $response->usage,
            provider: $response->provider,
            model: $response->model,
            latencyMs: $response->latencyMs,
        );
    }

    private function buildUserPrompt(Skill $skill, array $input): string
    {
        $template = $skill->configuration['prompt_template'] ?? null;

        if ($template) {
            // Simple variable substitution: {{variable_name}}
            return preg_replace_callback('/\{\{(\w+)\}\}/', function ($matches) use ($input) {
                return $input[$matches[1]] ?? $matches[0];
            }, $template);
        }

        return json_encode($input);
    }

    /**
     * @return array{execution: SkillExecution, output: null}
     */
    private function failExecution(
        Skill $skill,
        string $teamId,
        ?string $agentId,
        ?string $experimentId,
        array $input,
        string $errorMessage,
        int $durationMs = 0,
    ): array {
        $execution = SkillExecution::create([
            'skill_id' => $skill->id,
            'agent_id' => $agentId,
            'experiment_id' => $experimentId,
            'team_id' => $teamId,
            'status' => 'failed',
            'input' => $input,
            'output' => null,
            'duration_ms' => $durationMs,
            'cost_credits' => 0,
            'error_message' => $errorMessage,
        ]);

        return [
            'execution' => $execution,
            'output' => null,
        ];
    }
}
