<?php
/**
 * Colex AI Usage Engine
 *
 * The ONLY place in the application that understands provider-specific AI
 * usage metadata (Rule 1) and the ONLY place that knows how AI Usage Units
 * map to plan allowances. Everything else — chat.php, colex.php, any
 * future endpoint — talks to this file in terms of generic "usage units"
 * and never sees a token count, a provider name, or a cost figure.
 *
 * Swapping or adding an AI provider (GPT, Gemini, Grok, ...) means adding
 * one case to convertProviderUsageToUnits() below. Nothing outside this
 * file should need to change (Rule 8).
 *
 * ── Design notes / assumptions (please verify) ─────────────────────────
 * - Billing periods are calendar months (1st through last day), the same
 *   for every user, rather than a per-user signup-anniversary cycle. If
 *   TraderAlign's actual billing periods are anchored to signup/renewal
 *   date instead, adjust getCurrentBillingPeriod() below — everything
 *   else reads period boundaries from that one function.
 * - user_profiles.tier holds 'free'|'starter'|'pro'|'elite'|'ultimate'
 *   (same column used by Colex's product-knowledge context — see
 *   getUserPlanContext() in chat.php).
 * - colex_plan_limits and colex_usage are created by the accompanying SQL
 *   migration (colex_usage_engine_migration.sql). The initial allowance
 *   values inserted there are PLACEHOLDERS, same as everything else
 *   marked draft in this build — change them any time without touching
 *   this file, that's the whole point of Rule 2.
 * - The AI Usage Unit itself is an internal, arbitrary scale — there is no
 *   "correct" answer for how many units a Claude response should cost
 *   until real unit economics are decided. convertAnthropicUsageToUnits()
 *   below has a placeholder formula, clearly marked, ready to be tuned.
 */

// ── Billing Period ──────────────────────────────────────────

/**
 * The current billing period's boundaries, plus when the next one starts
 * (used as the "resets on" date shown to users). Calendar-month based —
 * see the assumption note above if that's not actually how TraderAlign
 * bills.
 *
 * There's no separate "period" row to create in the database — a period
 * is just a date range colex_usage rows get stamped with, so "creating
 * the next billing period" (Rule 6) happens implicitly the moment a new
 * usage event is recorded after the boundary passes. Historical rows keep
 * whatever period_start they were written with, so nothing needs to be
 * migrated or archived when a period rolls over.
 */
function getCurrentBillingPeriod() {
    $now = new DateTime();
    $start = new DateTime($now->format('Y-m-01'));
    $end = (clone $start)->modify('last day of this month');
    $nextStart = (clone $start)->modify('+1 month');

    return [
        'start' => $start->format('Y-m-d'),
        'end' => $end->format('Y-m-d'),
        'next_start' => $nextStart->format('Y-m-d'),
    ];
}

// ── Plan / Tier Lookup ──────────────────────────────────────

/**
 * Standalone tier lookup for use anywhere the usage engine is called
 * without an already-loaded plan context (e.g. colex.php on page load).
 * Mirrors getUserPlanContext()'s tier query in chat.php but doesn't
 * depend on that file, so the usage engine has no dependency in either
 * direction.
 */
function getUserTierForUsage($db, $userId) {
    try {
        $row = $db->raw("SELECT tier FROM user_profiles WHERE user_id = ? LIMIT 1", [$userId]);
        if (!empty($row) && !empty($row[0]['tier'])) {
            return strtolower(trim($row[0]['tier']));
        }
    } catch (Exception $e) {
        error_log("[Colex Usage Engine] Could not load tier (falling back to free): " . $e->getMessage());
    }
    return 'free';
}

/**
 * Monthly allowance for a tier, read from colex_plan_limits — this is
 * Rule 2: no allowance number is ever hard-coded here. If a tier has no
 * configured row (e.g. a new tier was added to user_profiles but not yet
 * priced), this fails safe to 0 rather than silently granting unlimited
 * usage, and logs loudly so it gets noticed.
 */
function getPlanAllowanceUnits($db, $tier) {
    try {
        $row = $db->raw(
            "SELECT monthly_allowance_units FROM colex_plan_limits WHERE tier = ? LIMIT 1",
            [$tier]
        );
        if (!empty($row)) {
            return (float)$row[0]['monthly_allowance_units'];
        }
    } catch (Exception $e) {
        error_log("[Colex Usage Engine] Could not load allowance for tier '{$tier}': " . $e->getMessage());
    }

    error_log("[Colex Usage Engine] No colex_plan_limits row for tier '{$tier}' — defaulting allowance to 0");
    return 0.0;
}

// ── Conversion Layer (Rule 1) ───────────────────────────────

/**
 * The single entry point every provider integration must call. Nothing
 * outside this function should ever branch on provider name — adding a
 * new provider means adding one case here and nowhere else.
 *
 * @param string $provider     e.g. 'anthropic'. Internal only — never
 *                              shown to users, never stored anywhere
 *                              users can see it.
 * @param array  $providerUsageRaw The raw usage object the provider's API
 *                              returned (e.g. Anthropic's `usage` block).
 * @return float AI Usage Units, this provider's response converted to the
 *               app's internal generic scale.
 */
function convertProviderUsageToUnits($provider, $providerUsageRaw) {
    switch ($provider) {
        case 'anthropic':
            return convertAnthropicUsageToUnits($providerUsageRaw);

        // Add future providers here, e.g.:
        // case 'openai':
        //     return convertOpenAIUsageToUnits($providerUsageRaw);
        // case 'google':
        //     return convertGoogleUsageToUnits($providerUsageRaw);

        default:
            error_log("[Colex Usage Engine] Unknown provider '{$provider}' — cannot convert usage, recording 0 units");
            return 0.0;
    }
}

/**
 * PLACEHOLDER conversion formula for Anthropic's Messages API usage block
 * ({input_tokens, output_tokens, cache_creation_input_tokens,
 * cache_read_input_tokens}). Output tokens are weighted heaviest since
 * they're the most expensive; cached input reads are weighted lightest.
 * The /100 scale factor is arbitrary — tune all of this once real unit
 * economics are decided. This is the only function in the entire
 * application that reads an Anthropic-shaped usage object.
 */
function convertAnthropicUsageToUnits($usageRaw) {
    $inputTokens = (float)($usageRaw['input_tokens'] ?? 0);
    $outputTokens = (float)($usageRaw['output_tokens'] ?? 0);
    $cacheWriteTokens = (float)($usageRaw['cache_creation_input_tokens'] ?? 0);
    $cacheReadTokens = (float)($usageRaw['cache_read_input_tokens'] ?? 0);

    $weightedTokens = ($inputTokens * 0.25)
        + ($outputTokens * 1.0)
        + ($cacheWriteTokens * 0.3)
        + ($cacheReadTokens * 0.1);

    $units = $weightedTokens / 100; // 100 weighted tokens ≈ 1 AI Usage Unit — PLACEHOLDER

    return round($units, 4);
}

// ── Recording Usage ──────────────────────────────────────────

/**
 * Records one AI response's usage. Called once per successful provider
 * call. Stores the converted units (what everything else in the app
 * uses) alongside the raw provider payload (kept for audit and in case
 * the conversion formula is revised later and historical usage needs
 * recomputing) and the internal feature tag (Rule 7 — never surfaced to
 * users, analytics/internal use only).
 *
 * @param string $feature One of: 'chat', 'voice_conversation',
 *                         'voice_notes', 'image_analysis',
 *                         'document_analysis', 'internet_research', or a
 *                         future capability's own tag. Free-form on
 *                         purpose so new capabilities don't require a
 *                         schema change — just start passing a new tag.
 * @return float The AI Usage Units recorded for this event.
 */
function recordColexUsage($db, $userId, $provider, $providerUsageRaw, $feature = 'chat') {
    $units = convertProviderUsageToUnits($provider, $providerUsageRaw);
    $period = getCurrentBillingPeriod();

    try {
        $db->raw(
            "INSERT INTO colex_usage 
                (id, user_id, period_start, period_end, feature, provider, provider_usage_raw, usage_units, created_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())",
            [
                bin2hex(random_bytes(16)),
                $userId,
                $period['start'],
                $period['end'],
                $feature,
                $provider,
                json_encode($providerUsageRaw),
                $units,
            ]
        );
    } catch (Exception $e) {
        // Usage recording failing should never break the chat response the
        // user already received — log loudly and move on. Worst case for
        // a transient DB hiccup is one under-counted event, not a broken
        // conversation.
        error_log("[Colex Usage Engine] Failed to record usage for user {$userId}: " . $e->getMessage());
    }

    return $units;
}

/**
 * Total AI Usage Units consumed by a user in the current billing period.
 */
function getUsedUnitsThisPeriod($db, $userId) {
    $period = getCurrentBillingPeriod();

    try {
        $row = $db->raw(
            "SELECT COALESCE(SUM(usage_units), 0) as total_units 
             FROM colex_usage 
             WHERE user_id = ? AND period_start = ?",
            [$userId, $period['start']]
        );
        if (!empty($row)) {
            return (float)$row[0]['total_units'];
        }
    } catch (Exception $e) {
        error_log("[Colex Usage Engine] Could not sum usage for user {$userId}: " . $e->getMessage());
    }

    return 0.0;
}

// ── Allowance Status (the only thing the UI should ever see) ──

/**
 * The full internal picture of a user's allowance standing. Everything
 * this returns is either safe to expose (remaining_percent, color,
 * exhausted, reset_date) or explicitly for backend/logging use only
 * (tier, allowance_units, used_units, remaining_units — never send these
 * three to the frontend; use formatAllowanceForFrontend() for that).
 */
function getColexAllowanceStatus($db, $userId) {
    $period = getCurrentBillingPeriod();
    $tier = getUserTierForUsage($db, $userId);
    $allowanceUnits = getPlanAllowanceUnits($db, $tier);
    $usedUnits = getUsedUnitsThisPeriod($db, $userId);
    $remainingUnits = max(0, $allowanceUnits - $usedUnits);

    $remainingPercent = $allowanceUnits > 0
        ? (int)round(($remainingUnits / $allowanceUnits) * 100)
        : 0;

    if ($remainingPercent > 50) {
        $color = 'green';
    } elseif ($remainingPercent >= 20) {
        $color = 'yellow';
    } else {
        $color = 'red';
    }

    $resetDate = (new DateTime($period['next_start']))->format('F j, Y');

    return [
        // Safe to send to the frontend as-is:
        'remaining_percent' => $remainingPercent,
        'color' => $color,
        'exhausted' => $remainingUnits <= 0,
        'reset_date' => $resetDate,
        // Backend/logging use only — do not expose in any API response:
        'tier' => $tier,
        'allowance_units' => $allowanceUnits,
        'used_units' => $usedUnits,
        'remaining_units' => $remainingUnits,
    ];
}

/**
 * Strips a full allowance status down to exactly what's safe to put in a
 * JSON response for the frontend — no unit counts, no tier name, nothing
 * that hints at how the number was calculated (Rule 3/9). Both chat.php
 * and colex.php should send this shape to the browser, never the raw
 * output of getColexAllowanceStatus().
 */
function formatAllowanceForFrontend($status) {
    return [
        'remaining_percent' => $status['remaining_percent'],
        'color' => $status['color'],
        'exhausted' => $status['exhausted'],
        'reset_date' => $status['reset_date'],
    ];
}

// ── Allowance Gate ───────────────────────────────────────────

/**
 * Call this before making any AI provider request. Returns false when the
 * user's allowance is exhausted, in which case the caller must not call
 * the provider at all — this is the actual enforcement point (Rule 4);
 * the UI disabling the input box is a courtesy, not the real gate, since
 * a request could otherwise be sent directly to the endpoint.
 */
function canSendColexMessage($db, $userId) {
    $status = getColexAllowanceStatus($db, $userId);
    return [!$status['exhausted'], $status];
}
