<?php
/**
 * سودانا — Kernel v1
 * The single mandatory orchestration layer.
 *
 * Every endpoint should use Kernel::handle() at the top to get:
 *   - Trace ID generation
 *   - Security gate (auth + CSRF + rate limit + IP block)
 *   - Pricing context loaded
 *   - Logger context primed
 *   - Auto JSON response with trace_id
 *
 * Usage in any endpoint:
 *
 *   require_once __DIR__.'/../../engine/Kernel.php';
 *   $ctx = Kernel::handle([
 *       'auth'   => true,
 *       'rate'   => 'api',
 *       'budget' => ['type'=>'document', 'service'=>'contract'],
 *   ]);
 *
 *   // ... business logic uses $ctx['user'], $ctx['trace_id']
 *
 *   Kernel::respond(['ok'=>true, 'data'=>$result]);
 */
require_once __DIR__ . '/Logger.php';
require_once __DIR__ . '/SecurityGate.php';
require_once __DIR__ . '/BudgetGate.php';
require_once __DIR__ . '/PricingEngine.php';
require_once __DIR__ . '/UsageMeter.php';


/**
 * KernelExecutionToken — enforces that ReservationContext is only created by Kernel.
 *
 * Pattern: Kernel generates a one-time-use secret before calling new ReservationContext().
 * ReservationContext validates the secret. Token is consumed (deleted) on first use.
 * No external code can predict or reuse the token.
 */
final class KernelExecutionToken {
    private static ?string $current = null;

    /** Called ONLY by Kernel — generates a fresh one-time token */
    public static function generate(): string {
        self::$current = bin2hex(random_bytes(16));
        return self::$current;
    }

    /** Called by ReservationContext — validates AND consumes the token */
    public static function consume(string $token): bool {
        if (self::$current === null || !hash_equals(self::$current, $token)) {
            return false;
        }
        self::$current = null; // consume — can never be reused
        return true;
    }

    /** Emergency reset (e.g., exception in Kernel before ReservationContext built) */
    public static function reset(): void { self::$current = null; }
}

final class Kernel {

    private static ?array $context = null;

    /**
     * The mandatory entry point. Configures the entire request lifecycle.
     *
     * @param array $opts {
     *   auth: bool|'admin'|'optional' (default true),
     *   csrf: bool (default false),
     *   rate: string (default 'api'),
     *   public: bool (default false),
     *   budget: array|null { type, service } (default null = no budget check),
     *   require_https: bool (default true on production),
     * }
     * @return array { user, trace_id, pricing }
     */
    public static function handle(array $opts = []): array {
        // 1. Initialize trace ID (or pick up from header)
        $incomingTrace = $_SERVER['HTTP_X_TRACE_ID'] ?? null;
        if ($incomingTrace) Logger::setTraceId($incomingTrace);
        $traceId = Logger::traceId();

        // 2. Set response headers
        if (!headers_sent()) {
            header('Content-Type: application/json; charset=utf-8');
            header('X-Trace-Id: ' . $traceId);
            header('X-Frame-Options: DENY');
            header('X-Content-Type-Options: nosniff');
            header('Referrer-Policy: strict-origin-when-cross-origin');
        }

        // 3. Security gate (auth, CSRF, rate, IP)
        $user = SecurityGate::enforce($opts);

        // 4. Budget gate (if requested)
        if (!empty($opts['budget']) && $user) {
            $b = $opts['budget'];
            BudgetGate::enforce(
                (int)$user['id'],
                $b['type'] ?? 'chat',
                $b['service'] ?? 'general',
                $b['opts'] ?? []
            );
        }

        // 5. Load pricing context (lazy via PricingEngine, cached)
        // Frontend can request it via /api/public/pricing.php — kernel doesn't pre-load

        // 6. Build context
        self::$context = [
            'user'     => $user,
            'trace_id' => $traceId,
            'started'  => microtime(true),
            'opts'     => $opts,
        ];

        // 7. Log request start
        Logger::info('request_start', [
            'method' => $_SERVER['REQUEST_METHOD'] ?? '?',
            'uri'    => $_SERVER['REQUEST_URI'] ?? '?',
        ]);

        return self::$context;
    }

    /**
     * Send a successful JSON response and exit.
     */
    public static function respond(array $payload, int $code = 200): void {
        $duration = self::$context
            ? round((microtime(true) - self::$context['started']) * 1000)
            : 0;

        Logger::info('request_end', ['code' => $code, 'ms' => $duration]);

        http_response_code($code);
        $payload['trace_id'] = Logger::traceId();
        echo json_encode($payload, JSON_UNESCAPED_UNICODE);
        exit;
    }

    /**
     * Send an error JSON response and exit.
     */
    public static function fail(string $error, string $message = '', int $code = 400, array $extra = []): void {
        Logger::warn('request_failed', ['error' => $error, 'code' => $code]);

        http_response_code($code);
        echo json_encode(array_merge([
            'error'    => $error,
            'message'  => $message,
            'trace_id' => Logger::traceId(),
        ], $extra), JSON_UNESCAPED_UNICODE);
        exit;
    }

    /**
     * Wrap an AI call with budget + cost tracking + auto-debit.
     * Convenience shortcut for endpoints that do AI work.
     *
     * @return array { result, charged_sdg, wallet_after, tokens_used }
     */
    public static function meteredAI(string $eventType, string $service,
                                      callable $aiCallback, array $opts = []): array {
        $user = self::$context['user'] ?? null;
        if (!$user) {
            self::fail('not_authenticated', 'يجب تسجيل الدخول لاستخدام AI', 401);
        }

        return BudgetGate::metered((int)$user['id'], $eventType, $service, $aiCallback, $opts);
    }

    /** Get current request context (set by handle()) */
    public static function context(): ?array {
        return self::$context;
    }

    /** Get the current authenticated user (or null) */
    public static function user(): ?array {
        return self::$context['user'] ?? null;
    }

    /** Get the trace ID for the current request */
    public static function traceId(): string {
        return Logger::traceId();
    }

    /**
     * Read JSON body safely.
     * Returns [] on parse failure rather than throwing.
     */
    public static function jsonBody(): array {
        static $cached = null;
        if ($cached !== null) return $cached;

        $raw = file_get_contents('php://input');
        if (!$raw) return $cached = [];

        $data = json_decode($raw, true);
        return $cached = (is_array($data) ? $data : []);
    }

    /**
     * Validate required fields in JSON body. Fails request if missing.
     */
    public static function require(array $fields): array {
        $body = self::jsonBody();
        $missing = [];
        foreach ($fields as $f) {
            if (!isset($body[$f]) || $body[$f] === '') $missing[] = $f;
        }
        if ($missing) {
            self::fail('missing_fields', 'الحقول التالية مطلوبة: ' . implode(', ', $missing), 400,
                       ['missing' => $missing]);
        }
        return $body;
    }

    // ════════════════════════════════════════════════════════════
    //  ESTIMATION INTEGRATION
    // ════════════════════════════════════════════════════════════

    /**
     * Full pre-execution estimation flow:
     *   1. Estimate with TaskEstimator
     *   2. Check budget (with soft reservation accounting)
     *   3. Hold reservation if approved
     *   4. Return { action:'confirm', plan, token } or 402
     *
     * Usage in any endpoint:
     *   $ctx = Kernel::handle(['auth' => true]);
     *   return Kernel::estimateAndHold($ctx['user'], $prompt);
     */
    public static function estimateAndHold(array $user, string $prompt): void {
        require_once __DIR__ . '/TaskEstimator.php';
        require_once __DIR__ . '/ReservationEngine.php';
        require_once __DIR__ . '/BudgetGate.php';
        require_once __DIR__ . '/PricingEngine.php';

        $userId   = (int)$user['id'];
        $userPlan = $user['plan_key'] ?? $user['subscription_tier'] ?? 'free';

        // ── 1. Estimate ──────────────────────────────────────
        try {
            $plan = \TaskEstimator::analyze($prompt, $userId, $userPlan);
        } catch (\RuntimeException $e) {
            self::fail(
                $e->getMessage(),
                match($e->getMessage()) {
                    'estimation_rate_limited'     => 'تجاوزت حد طلبات التقدير. انتظر دقيقة.',
                    'estimation_daily_cap_reached' => 'وصلت للحد اليومي من التقديرات.',
                    default                        => 'تعذّر تحليل الطلب.',
                },
                429
            );
        }

        // ── 2. Budget check (with soft reservation accounting) ─
        $budget = \BudgetGate::canAffordEstimate($userId, $plan);

        if (!$budget['ok']) {
            http_response_code(402);
            echo json_encode([
                'ok'            => false,
                'error'         => $budget['error'] ?? 'insufficient_funds',
                'required_sdg'  => $budget['required_sdg'],
                'available_sdg' => $budget['available_sdg'],
                'shortfall_sdg' => $budget['shortfall_sdg'],
                'breakdown'     => $budget['breakdown'],
                'plan'          => $plan,
                'suggestions'   => $budget['suggestions'] ?? [],
                'trace_id'      => \Logger::traceId(),
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        // ── 3. Hold reservation ───────────────────────────────
        $ttl = (int)\PricingEngine::getSetting('estimation_ttl_minutes', 30);
        $reservation = \ReservationEngine::hold($userId, $plan, $ttl);

        // ── 4. Respond ────────────────────────────────────────
        self::respond([
            'ok'                    => true,
            'action'                => 'awaiting_confirmation',   // machine code
            'action_label'          => 'راجع التكلفة واضغط تأكيد للمتابعة',
            'plan'                  => $plan,
            'cost'                  => $plan['cost'],
            'wallet_after_confirm'  => max(0, $budget['breakdown']['available'] - $plan['cost']['total_sdg']),
            'reservation_id'        => $reservation['reservation_id'],
            'token'                 => $reservation['confirmation_token'],
            'expires_in_seconds'    => $ttl * 60,
            'expires_at'            => $reservation['expires_at'],
        ]);
    }

    /**
     * Execute a confirmed reservation (user clicked "confirm").
     * Validates token, activates reservation, runs TaskRunner.
     */
    public static function executeWithReservation(string $token): void {
        require_once __DIR__ . '/ReservationEngine.php';
        require_once __DIR__ . '/TaskRunner.php';

        // ── Activate (validates token, debits, returns context) ─
        try {
            $activationResult = \ReservationEngine::activate($token);
        } catch (\RuntimeException $e) {
            self::fail(
                $e->getMessage(),
                match($e->getMessage()) {
                    'invalid_or_expired_token'   => 'الرمز غير صالح أو انتهت صلاحيته.',
                    'price_snapshot_expired'     => 'انتهت صلاحية التقدير. أعد الطلب.',
                    'reservation_already_activated' => 'تم تنفيذ هذا الطلب مسبقاً.',
                    default                      => 'خطأ في تفعيل الحجز.',
                },
                400
            );
        }

        // ── Build context with one-time Kernel token ──────────────
        // Token proves this ReservationContext was created by Kernel::executeWithReservation
        // Token is consumed in ReservationContext constructor — cannot be reused
        $kernelToken = \KernelExecutionToken::generate();
        try {
            $ctx = new \ReservationContext($activationResult, $kernelToken);
        } catch (\RuntimeException $e) {
            \KernelExecutionToken::reset(); // clean up on failure
            throw $e;
        }
        $plan   = $activationResult['plan'];
        $runner = new \TaskRunner($ctx, $plan);

        // ── Execute ───────────────────────────────────────────
        $result = $runner->run();

        self::respond([
            'ok'              => $result['ok'],
            'tasks_completed' => $result['tasks_completed'],
            'tasks_failed'    => $result['tasks_failed'],
            'reserved_sdg'    => $result['reserved_sdg'],
            'actual_sdg'      => $result['actual_sdg'],
            'refunded_sdg'    => $result['refunded_sdg'],
            'results'         => $result['results'],
        ]);
    }

}