<?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,
    ) {}

    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 {
        // Specialized skill types (code/browser/runpod/gpu/boruna/supabase) own their
        // full pipeline — delegate and return early when one matches.
        $delegated = $this->delegateSpecializedType($skill, $input, $teamId, $userId, $agentId, $experimentId);
        if ($delegated !== null) {
            return $delegated;
        }

        // 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);

            // 5. Validate output if schema defined — with retry-on-failure for
            //    LLM-backed skill types (Sprint 14, mirrors Agent Sprint 12).
            $validated = $this->validateOutputWithSchemaRetry(
                $skill, $input, $resolvedProvider, $resolvedModel, $teamId, $userId, $agentId, $experimentId, $response, $startTime,
            );
            $response = $validated['response'];
            $output = $validated['output'];

            // 6. Create execution record
            $executionData = $this->buildExecutionData($skill, $input, $output, $validated['durationMs'], $response, $agentId, $experimentId, $teamId, $validated['schemaValid'], $validated['attempts'], $validated['trail']);
            $execution = SkillExecution::create($executionData);

            // 7. Update skill stats
            $skill->recordExecution(true, $validated['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;
        }
    }

    /**
     * Skill types that own their full execution pipeline (worktree/sandbox/REST/GPU)
     * and bypass the LLM+budget flow. Returns the delegate's result, or null when
     * the skill is a standard (LLM/connector/rule/consensus) type.
     *
     * @param  array<string, mixed>  $input
     * @return array{execution: SkillExecution, output: array|string|null}|null
     */
    private function delegateSpecializedType(Skill $skill, array $input, string $teamId, string $userId, ?string $agentId, ?string $experimentId): ?array
    {
        $delegate = match ($skill->type) {
            SkillType::CodeExecution->value => $this->executeCodeExecution,
            SkillType::Browser->value => $this->executeBrowserSkill,
            SkillType::RunpodEndpoint->value => $this->executeRunPod,
            SkillType::RunpodPod->value => $this->executeRunPodPod,
            SkillType::GpuCompute->value => $this->executeGpuCompute,
            SkillType::BorunaScript->value => $this->executeBorunaScript,
            SkillType::SupabaseEdgeFunction->value => $this->executeSupabaseEdgeFunction,
            default => null,
        };

        return $delegate?->execute($skill, $input, $teamId, $userId, $agentId, $experimentId);
    }

    /**
     * Validate the response output against the skill's output_schema, re-prompting
     * the LLM up to max-retries when invalid (LLM-backed skill types only).
     *
     * @param  array<string, mixed>  $input
     * @return array{response: AiResponseDTO, output: mixed, schemaValid: bool, attempts: int, trail: array<int, array<string, mixed>>, durationMs: int}
     */
    private function validateOutputWithSchemaRetry(
        Skill $skill,
        array $input,
        string $provider,
        string $model,
        string $teamId,
        string $userId,
        ?string $agentId,
        ?string $experimentId,
        AiResponseDTO $response,
        int $startTime,
    ): array {
        $output = $response->parsedOutput ?? json_decode($response->content, true);
        $durationMs = (int) ((hrtime(true) - $startTime) / 1_000_000);
        $schemaValid = true;
        $attempts = 0;
        $trail = [];

        if (empty($skill->output_schema) || ! is_array($output)) {
            return compact('response', 'output', 'schemaValid', 'attempts', 'trail', 'durationMs');
        }

        $outputValidation = $this->schemaValidator->validate($output, $skill->output_schema);
        $schemaValid = $outputValidation['valid'];

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

                try {
                    $response = $this->executeLlmRetry(
                        skill: $skill,
                        input: $input,
                        provider: $provider,
                        model: $model,
                        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' => $attempts,
                        'error' => $e->getMessage(),
                    ]);
                    break;
                }

                $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);
        }

        return compact('response', 'output', 'schemaValid', 'attempts', 'trail', 'durationMs');
    }

    /**
     * Assemble the SkillExecution attributes, including MultiModelConsensus quality
     * columns and the schema-retry trail when self-correction occurred.
     *
     * @param  array<string, mixed>  $input
     * @param  array<int, array<string, mixed>>  $schemaRetryTrail
     * @return array<string, mixed>
     */
    private function buildExecutionData(
        Skill $skill,
        array $input,
        mixed $output,
        int $durationMs,
        AiResponseDTO $response,
        ?string $agentId,
        ?string $experimentId,
        string $teamId,
        bool $schemaValid,
        int $schemaRetryAttempts,
        array $schemaRetryTrail,
    ): array {
        $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->value && 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) {
            // quality_details is not set elsewhere in this freshly-built array.
            $executionData['quality_details'] = [
                'output_schema_valid' => $schemaValid,
                'output_schema_retries' => $schemaRetryAttempts,
                'output_schema_retry_trail' => $schemaRetryTrail,
            ];
        }

        return $executionData;
    }

    /**
     * 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 => throw new \LogicException('CodeExecution, Browser, RunpodEndpoint, RunpodPod, GpuCompute, and BorunaScript 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,
        ];
    }
}
