<?php

namespace App\Infrastructure\AI\DTOs;

use App\Infrastructure\AI\Enums\BudgetPressureLevel;
use App\Infrastructure\AI\Enums\ReasoningEffort;
use App\Infrastructure\AI\Enums\RequestComplexity;
use Prism\Prism\Schema\ObjectSchema;
use Prism\Prism\Tool;

final readonly class AiRequestDTO
{
    public function __construct(
        public string $provider,
        public string $model,
        public string $systemPrompt,
        public string $userPrompt,
        public int $maxTokens = 4096,
        public ?ObjectSchema $outputSchema = null,
        public ?string $userId = null,
        public ?string $teamId = null,
        public ?string $experimentId = null,
        public ?string $experimentStageId = null,
        public ?string $agentId = null,
        public ?string $purpose = null,
        public ?string $idempotencyKey = null,
        public float $temperature = 0.7,
        public ?array $fallbackChain = null,
        /** @var array<Tool>|null */
        public ?array $tools = null,
        public int $maxSteps = 1,
        public ?string $toolChoice = null,
        /** Custom endpoint credential name (only when provider='custom_endpoint') */
        public ?string $providerName = null,
        /** Anthropic extended thinking budget in tokens (null = disabled, only used when provider='anthropic') */
        public ?int $thinkingBudget = null,
        /** High-level reasoning effort hint — Auto resolves at runtime via BudgetPressureRouting, others map to fixed token budgets */
        public ?ReasoningEffort $effort = null,
        /** Per-call working directory override for local agent execution (e.g. a specific repo path) */
        public ?string $workingDirectory = null,
        /** Enable Anthropic prompt caching — marks system prompt and tool definitions with cache_control: ephemeral */
        public bool $enablePromptCaching = false,
        /** Explicit complexity hint — overrides heuristic classification when set */
        public ?RequestComplexity $complexity = null,
        /** Computed complexity after classification (set by BudgetPressureRouting middleware) */
        public ?RequestComplexity $classifiedComplexity = null,
        /** Budget pressure level at time of request (set by BudgetPressureRouting middleware) */
        public ?BudgetPressureLevel $budgetPressureLevel = null,
        /** Number of model-tier escalation attempts made (set by FallbackAiGateway) */
        public int $escalationAttempts = 0,
        /** Opt into Anthropic Fast Mode beta header for this call (ignored for non-Anthropic providers or if globally disabled) */
        public bool $fastMode = false,
        /**
         * Per-request BYOK override — raw provider API key supplied by an internal caller
         * (e.g. Partner Program, finance sub-program). When set, wins over team BYOK credential
         * lookup for this single call. Never persisted (excluded from logs, audit, idempotency).
         * Capped at 512 chars to keep error-message truncation guarantees.
         *
         * @internal Sensitive — must be stripped from any serialization (Horizon payloads, audit, Sentry).
         */
        public ?string $providerCredentialOverride = null,
        /** Reorder fallback chain by 24h-rolling provider metric — 'cost' | 'latency' | null (off, default). */
        public ?string $gatewaySort = null,
        /**
         * Phoenix-trace parent span context. Populated by callers that wrap a
         * higher-level unit of work (agent.execute, playbook.step, crew.execute)
         * so child LLM spans nest under it.
         *
         * 32-hex traceId, 16-hex spanId per OTel spec. When null, the export
         * middleware creates a stand-alone span.
         */
        public ?string $parentTraceId = null,
        public ?string $parentSpanId = null,
        /**
         * Caller-specified hard ceiling, in platform_credits, for THIS single call.
         * Enforced before the provider is hit (alongside the standing per-call cap):
         * the effective cap is the lower of this and the team/agent standing cap.
         * Null = no per-request ceiling (standing cap still applies).
         */
        public ?int $maxCostCredits = null,
    ) {}

    /**
     * Clone with a different model id, preserving every other field. Used by the
     * gateway to swap in a provider-specific model id (e.g. OpenRouter aliasing)
     * without dropping any request state. Covered by AiRequestDtoWithModelTest,
     * which fails if a newly added constructor field is missed here.
     */
    public function withModel(string $model): self
    {
        return new self(
            provider: $this->provider,
            model: $model,
            systemPrompt: $this->systemPrompt,
            userPrompt: $this->userPrompt,
            maxTokens: $this->maxTokens,
            outputSchema: $this->outputSchema,
            userId: $this->userId,
            teamId: $this->teamId,
            experimentId: $this->experimentId,
            experimentStageId: $this->experimentStageId,
            agentId: $this->agentId,
            purpose: $this->purpose,
            idempotencyKey: $this->idempotencyKey,
            temperature: $this->temperature,
            fallbackChain: $this->fallbackChain,
            tools: $this->tools,
            maxSteps: $this->maxSteps,
            toolChoice: $this->toolChoice,
            providerName: $this->providerName,
            thinkingBudget: $this->thinkingBudget,
            effort: $this->effort,
            workingDirectory: $this->workingDirectory,
            enablePromptCaching: $this->enablePromptCaching,
            complexity: $this->complexity,
            classifiedComplexity: $this->classifiedComplexity,
            budgetPressureLevel: $this->budgetPressureLevel,
            escalationAttempts: $this->escalationAttempts,
            fastMode: $this->fastMode,
            providerCredentialOverride: $this->providerCredentialOverride,
            gatewaySort: $this->gatewaySort,
            parentTraceId: $this->parentTraceId,
            parentSpanId: $this->parentSpanId,
            maxCostCredits: $this->maxCostCredits,
        );
    }

    /**
     * Clone with a different user prompt, preserving every other field. Used by
     * SchemaValidation to issue a self-correcting retry (re-prompt for valid JSON)
     * without dropping any request state. Covered by AiRequestDtoWithUserPromptTest,
     * which fails if a newly added constructor field is missed here.
     */
    public function withUserPrompt(string $userPrompt): self
    {
        return new self(
            provider: $this->provider,
            model: $this->model,
            systemPrompt: $this->systemPrompt,
            userPrompt: $userPrompt,
            maxTokens: $this->maxTokens,
            outputSchema: $this->outputSchema,
            userId: $this->userId,
            teamId: $this->teamId,
            experimentId: $this->experimentId,
            experimentStageId: $this->experimentStageId,
            agentId: $this->agentId,
            purpose: $this->purpose,
            idempotencyKey: $this->idempotencyKey,
            temperature: $this->temperature,
            fallbackChain: $this->fallbackChain,
            tools: $this->tools,
            maxSteps: $this->maxSteps,
            toolChoice: $this->toolChoice,
            providerName: $this->providerName,
            thinkingBudget: $this->thinkingBudget,
            effort: $this->effort,
            workingDirectory: $this->workingDirectory,
            enablePromptCaching: $this->enablePromptCaching,
            complexity: $this->complexity,
            classifiedComplexity: $this->classifiedComplexity,
            budgetPressureLevel: $this->budgetPressureLevel,
            escalationAttempts: $this->escalationAttempts,
            fastMode: $this->fastMode,
            providerCredentialOverride: $this->providerCredentialOverride,
            gatewaySort: $this->gatewaySort,
            parentTraceId: $this->parentTraceId,
            parentSpanId: $this->parentSpanId,
            maxCostCredits: $this->maxCostCredits,
        );
    }

    public function isStructured(): bool
    {
        return $this->outputSchema !== null;
    }

    public function hasTools(): bool
    {
        return ! empty($this->tools);
    }

    public function generateIdempotencyKey(): string
    {
        return $this->idempotencyKey ?? hash('xxh128', implode('|', [
            $this->provider,
            $this->model,
            $this->systemPrompt,
            $this->userPrompt,
            $this->experimentId ?? '',
            $this->experimentStageId ?? '',
            $this->purpose ?? '',
            (string) ($this->thinkingBudget ?? ''),
            $this->effort->value ?? '',
        ]));
    }
}
