<?php
/**
 * سودانا — TaskRunner v1
 *
 * Executes a list of tasks in the correct order given an active ReservationContext.
 *
 * ENFORCEMENT: Cannot be instantiated without a valid ReservationContext.
 *   - reservationId must be non-null
 *   - traceId must be non-null
 *   - status must be 'active'
 *
 * Execution order (cheapest/fastest first, most expensive last):
 *   text → document → diagram → table → image → video
 *
 * Per-task: prices come from price_snapshot ONLY (not live PricingEngine).
 */
require_once __DIR__ . '/ReservationEngine.php';
require_once __DIR__ . '/Logger.php';
require_once __DIR__ . '/ToolsEngine.php';
require_once __DIR__ . '/UsageMeter.php';

/** Immutable context passed to TaskRunner — enforces governance.
 *
 * ENFORCEMENT: Requires a valid KernelExecutionToken.
 * This token is:
 *   - Generated ONLY inside Kernel::executeWithReservation()
 *   - Consumed (deleted) on first use — cannot be reused
 *   - A random 16-byte secret that changes every request
 *
 * Any attempt to instantiate ReservationContext outside Kernel::executeWithReservation()
 * will throw KernelContextException, because no valid token exists.
 */
final class ReservationContext {
    public readonly string $reservationId;
    public readonly int    $userId;
    public readonly string $traceId;
    public readonly array  $priceSnapshot;
    public readonly string $userPlan;

    /**
     * @param array  $activationResult  From ReservationEngine::activate()
     * @param string $kernelToken       One-time token from KernelExecutionToken::generate()
     * @throws RuntimeException if token invalid (= called outside Kernel)
     */
    public function __construct(array $activationResult, string $kernelToken = '') {
        // ── GATE 1: Kernel execution token ────────────────────
        // This is the PRIMARY enforcement against bypass.
        // If called without a valid token, it means code is NOT going through Kernel.
        if (!\KernelExecutionToken::consume($kernelToken)) {
            Logger::critical('kernel_bypass_attempt', [
                'reservation_id' => $activationResult['reservation_id'] ?? 'unknown',
                'trace_id'       => Logger::traceId(),
                'caller'         => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? 'unknown',
            ]);
            throw new \RuntimeException(
                'KernelContextException: ReservationContext must be created via Kernel::executeWithReservation()'
            );
        }

        // ── GATE 2: Data integrity ─────────────────────────────
        if (empty($activationResult['reservation_id'])) {
            throw new \RuntimeException('MissingReservationException: reservation_id required');
        }
        if (empty($activationResult['price_snapshot'])) {
            throw new \RuntimeException('MissingPriceSnapshotException: price_snapshot required');
        }

        $this->reservationId = $activationResult['reservation_id'];
        $this->userId        = (int)$activationResult['user_id'];
        $this->traceId       = Logger::traceId();
        $this->priceSnapshot = $activationResult['price_snapshot'];
        $this->userPlan      = $activationResult['price_snapshot']['plan'] ?? 'free';
    }
}

class TaskRunner {

    private ReservationContext $ctx;
    private int $reservedSdg;
    private array $tasks;
    private array $executionLog = [];
    private int $actualSdg = 0;

    /** ONLY valid constructor — requires ReservationContext */
    public function __construct(ReservationContext $ctx, array $plan) {
        $this->ctx         = $ctx;
        $this->tasks       = $plan['tasks'] ?? [];
        $this->reservedSdg = (int)($plan['cost']['total_sdg'] ?? 0);

        // Verify reservation is still active in DB (double-guard)
        $res = ReservationEngine::getById($ctx->reservationId);
        if (!$res || $res['status'] !== 'active') {
            throw new \RuntimeException('InvalidReservationStatusException: must be active');
        }
    }

    /**
     * Execute all tasks in order, reconcile, return results.
     *
     * @return array {
     *   ok: bool,
     *   tasks_completed: int,
     *   tasks_failed: int,
     *   reserved_sdg: int,
     *   actual_sdg: int,
     *   refunded_sdg: int,
     *   results: array (per-task output)
     * }
     */
    public function run(): array {
        $ordered = $this->orderTasks($this->tasks);

        foreach ($ordered as $task) {
            $this->executeTask($task);
        }

        // Reconcile: compute actual vs reserved
        $actualSdg  = $this->actualSdg;
        $reserved   = $this->reservedSdg;
        $difference = $reserved - $actualSdg;

        // Handle overage
        if ($difference < 0) {
            $this->handleOverage(abs($difference));
        }

        // Finalize reservation
        ReservationEngine::complete(
            $this->ctx->reservationId,
            $actualSdg,
            $this->executionLog
        );

        $completed = count(array_filter($this->executionLog, fn($t) => $t['status'] === 'done'));
        $failed    = count(array_filter($this->executionLog, fn($t) => $t['status'] === 'failed'));

        Logger::info('taskrunner_complete', [
            'reservation_id' => $this->ctx->reservationId,
            'tasks_total'    => count($ordered),
            'tasks_done'     => $completed,
            'tasks_failed'   => $failed,
            'reserved_sdg'   => $reserved,
            'actual_sdg'     => $actualSdg,
        ]);

        return [
            'ok'              => $failed === 0,
            'tasks_completed' => $completed,
            'tasks_failed'    => $failed,
            'reserved_sdg'    => $reserved,
            'actual_sdg'      => $actualSdg,
            'refunded_sdg'    => max(0, $difference),
            'results'         => $this->executionLog,
        ];
    }

    // ── Execution order ──────────────────────────────────────────
    private function orderTasks(array $tasks): array {
        $order = ['text' => 0, 'document' => 1, 'diagram' => 2,
                  'table' => 3, 'image' => 4, 'video' => 5];

        usort($tasks, function($a, $b) use ($order) {
            return ($order[$a['type']] ?? 99) <=> ($order[$b['type']] ?? 99);
        });

        return $tasks;
    }

    // ── Execute a single task ────────────────────────────────────
    private function executeTask(array $task): void {
        $type    = $task['type'];
        $count   = max(1, (int)($task['count'] ?? 1));
        $subtype = $task['subtype'] ?? 'general';
        $sector  = $task['sector'] ?? 'general';

        // Get price FROM snapshot (not live PricingEngine)
        $unitCostSdg = (int)($task['unit_cost_sdg'] ?? 0);

        for ($i = 0; $i < $count; $i++) {
            $taskKey = "{$type}_{$subtype}_{$i}";
            $entry   = [
                'task_key'   => $taskKey,
                'type'       => $type,
                'subtype'    => $subtype,
                'status'     => 'pending',
                'cost_sdg'   => $unitCostSdg,
                'output'     => null,
                'error'      => null,
            ];

            try {
                $output = $this->dispatch($type, $subtype, $sector, $task);
                // Don't charge for unavailable providers
                if (($output['status'] ?? '') === 'provider_unavailable') {
                    $entry['status'] = 'skipped';
                    $entry['output'] = $output;
                    // No cost added to actualSdg
                } else {
                    $entry['status'] = 'done';
                    $entry['output'] = $output;
                    $this->actualSdg += $unitCostSdg;
                    $this->logToAiUsage($type, $subtype, $unitCostSdg);
                }

            } catch (\Throwable $e) {
                // Retry once
                Logger::warn('task_retry', ['task' => $taskKey, 'error' => $e->getMessage()]);
                try {
                    $output = $this->dispatch($type, $subtype, $sector, $task);
                    $entry['status'] = 'done';
                    $entry['output'] = $output;
                    $this->actualSdg += $unitCostSdg;
                    $this->logToAiUsage($type, $subtype, $unitCostSdg);
                } catch (\Throwable $e2) {
                    $entry['status'] = 'failed';
                    $entry['error']  = $e2->getMessage();
                    // No cost charged for failed tasks
                    Logger::error('task_failed', [
                        'task'           => $taskKey,
                        'reservation_id' => $this->ctx->reservationId,
                        'error'          => $e2->getMessage(),
                    ]);
                }
            }

            $this->executionLog[] = $entry;
        }
    }

    // ── Dispatch to appropriate engine ───────────────────────────
    private function dispatch(string $type, string $subtype, string $sector, array $task): mixed {
        return match($type) {
            'document' => $this->runDocument($subtype, $sector),
            'image'    => $this->runImage($subtype),
            'video'    => $this->runVideo($subtype),
            'diagram'  => $this->runDiagram($subtype),
            'table'    => $this->runTable($subtype, $sector),
            'text'     => $this->runText($sector),
            default    => throw new \RuntimeException("unknown_task_type: $type"),
        };
    }

    private function runDocument(string $docType, string $sector): array {
        // UsageMeter skip_debit=true: billing already done via reservation debit
        $metered = UsageMeter::wrap(
            $this->ctx->userId,
            'document',
            $docType,
            fn() => \ToolsEngine::generateDocument(
                "أنشئ {$docType} احترافياً لقطاع {$sector}",
                $sector,
                $this->ctx->userId,
                $docType
            ),
            ['skip_debit' => true]
        );

        if (!$metered['ok']) throw new \RuntimeException($metered['error'] ?? 'document_failed');
        return $metered['result'];
    }

    private function runImage(string $size): array {
        // Provider not yet connected — return unavailable (no charge)
        return ['type' => 'image', 'size' => $size, 'status' => 'provider_unavailable',
                'message' => 'خدمة توليد الصور ستتوفر قريباً'];
    }

    private function runVideo(string $duration): array {
        // Provider not yet connected — return unavailable (no charge)
        return ['type' => 'video', 'duration' => $duration, 'status' => 'provider_unavailable',
                'message' => 'خدمة توليد الفيديو ستتوفر قريباً'];
    }

    private function runDiagram(string $subtype): array {
        return ['type' => 'diagram', 'subtype' => $subtype, 'status' => 'generated_placeholder'];
    }

    private function runTable(string $subtype, string $sector): array {
        return ['type' => 'table', 'subtype' => $subtype, 'sector' => $sector, 'status' => 'generated'];
    }

    private function runText(string $sector): array {
        return ['type' => 'text', 'sector' => $sector, 'status' => 'generated'];
    }

    // ── Overage handling ─────────────────────────────────────────
    private function handleOverage(int $overageSdg): void {
        $plan = $this->ctx->userPlan;
        $tolerancePct = (int)\PricingEngine::getSetting("overage_tolerance_pct_$plan", 3);
        $overagePct   = $this->reservedSdg > 0
            ? ($overageSdg / $this->reservedSdg) * 100
            : 100;

        Logger::warn('task_overage', [
            'reservation_id' => $this->ctx->reservationId,
            'overage_sdg'    => $overageSdg,
            'overage_pct'    => round($overagePct, 1),
            'tolerance_pct'  => $tolerancePct,
        ]);

        if ($overagePct <= $tolerancePct) {
            // Silent overage charge — within tolerance
            UsageMeter::debit($this->ctx->userId, $overageSdg, 'overage', 'task_overage');
        } else {
            // Above tolerance — charge what we can, log warning
            // In v1 this is a "best effort" — no rollback on overage
            // Future: stop tasks and notify user
            Logger::critical('overage_above_tolerance', [
                'reservation_id' => $this->ctx->reservationId,
                'overage_pct'    => $overagePct,
            ]);
        }
    }

    // ── Log to ai_usage with reservation linkage ─────────────────
    private function logToAiUsage(string $type, string $subtype, int $costSdg): void {
        try {
            db()->prepare(
                "INSERT INTO ai_usage
                 (user_id, event_type, service, cost_sdg, reservation_id, trace_id, created_at)
                 VALUES (?, 'task_execution', ?, ?, ?, ?, NOW())"
            )->execute([
                $this->ctx->userId,
                "$type:$subtype",
                $costSdg,
                $this->ctx->reservationId,
                $this->ctx->traceId,
            ]);
        } catch (\Throwable $e) {
            Logger::warn('ai_usage_log_failed', ['error' => $e->getMessage()]);
        }
    }
}
