<?php

declare(strict_types=1);

namespace LolaCore\Telemetry;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * LolaTimer — request-scoped latency telemetry.
 *
 * DEV-ONLY TOOLING. This whole file and every `// LOLA_PERF` call-site across
 * the codebase exists to support the latency-optimization sprints
 * (IMPLEMENTATION_PLAN_LATENCY.md). After v1.3.0 metrics confirm the targets,
 * a teardown task must delete this folder and grep-remove the call-sites.
 *
 * Gated by WP_DEBUG. Production with WP_DEBUG=false pays zero cost
 * (every public method returns early before doing any work).
 *
 * Single-line output format (one line per request, emitted on shutdown):
 *   [LolaCore Perf] session=abc route=45ms|L2 prompt=320ms llm=2340ms|calls=2 facts=3100ms tools=0ms|calls=0 total=5805ms
 */
final class LolaTimer
{
    /**
     * Marker state.
     *
     * @var array<string, array{
     *     start: float|null,
     *     elapsed_ms: float,
     *     detail: string|null
     * }>
     */
    private static array $markers = [];

    /** @var array<string, int> */
    private static array $counters = [];

    /** @var array<string, mixed> */
    private static array $tags = [];

    private static bool $flushed = false;
    private static ?float $requestStart = null;

    /**
     * Master gate: is telemetry collection enabled for this request?
     * Returns true only when WP_DEBUG is on. All public methods short-circuit
     * if this is false, so production overhead is a single constant check.
     */
    private static function enabled(): bool
    {
        return defined('WP_DEBUG') && WP_DEBUG;
    }

    /**
     * Reset all state. Called at the start of each request that we want to
     * measure (typically from Controller::handleSendMessage and siblings).
     */
    public static function reset(): void
    {
        if (!self::enabled()) {
            return;
        }
        self::$markers = [];
        self::$counters = [];
        self::$tags = [];
        self::$flushed = false;
        self::$requestStart = microtime(true);
    }

    /**
     * Tag this request with arbitrary key/value pairs (e.g. session id, route).
     * Tags are rendered in the flushed line for grep/filter convenience.
     */
    public static function tag(string $key, $value): void
    {
        if (!self::enabled()) {
            return;
        }
        self::$tags[$key] = $value;
    }

    /**
     * Start a named marker. If `begin` is called twice for the same marker
     * without an intervening `end`, the second call wins (defensive).
     */
    public static function begin(string $marker): void
    {
        if (!self::enabled()) {
            return;
        }
        if (!isset(self::$markers[$marker])) {
            self::$markers[$marker] = [
                'start' => microtime(true),
                'elapsed_ms' => 0.0,
                'detail' => null,
            ];
        } else {
            self::$markers[$marker]['start'] = microtime(true);
        }
    }

    /**
     * End a marker. Accumulates elapsed time, allowing the same marker to be
     * called multiple times in a loop (e.g. `llm` across multiple turns).
     *
     * @param string               $marker Marker name (must match `begin`).
     * @param array<string, mixed> $detail Optional detail (e.g. ['layer' => 'L2']).
     *                                     Most recent non-empty detail wins.
     */
    public static function end(string $marker, array $detail = []): void
    {
        if (!self::enabled()) {
            return;
        }
        if (!isset(self::$markers[$marker]) || self::$markers[$marker]['start'] === null) {
            return;
        }
        $elapsed = (microtime(true) - self::$markers[$marker]['start']) * 1000.0;
        self::$markers[$marker]['elapsed_ms'] += $elapsed;
        self::$markers[$marker]['start'] = null;
        if (!empty($detail)) {
            self::$markers[$marker]['detail'] = self::formatDetail($detail);
        }
    }

    /**
     * Increment a named counter (e.g. `llm_calls`, `tool_calls`).
     */
    public static function increment(string $counter, int $by = 1): void
    {
        if (!self::enabled()) {
            return;
        }
        self::$counters[$counter] = (self::$counters[$counter] ?? 0) + $by;
    }

    /**
     * Manually record a measurement without using begin/end.
     * Useful when timing is already known (e.g. from an SDK callback).
     */
    public static function mark(string $marker, float $elapsedMs, array $detail = []): void
    {
        if (!self::enabled()) {
            return;
        }
        if (!isset(self::$markers[$marker])) {
            self::$markers[$marker] = [
                'start' => null,
                'elapsed_ms' => 0.0,
                'detail' => null,
            ];
        }
        self::$markers[$marker]['elapsed_ms'] += $elapsedMs;
        if (!empty($detail)) {
            self::$markers[$marker]['detail'] = self::formatDetail($detail);
        }
    }

    /**
     * Emit the single-line summary to error_log. Idempotent — calling twice
     * in the same request is a no-op (the shutdown hook may fire after a
     * manual flush, or vice versa).
     *
     * Only emits a line if at least one marker or counter has been recorded.
     */
    public static function flush(): void
    {
        if (!self::enabled() || self::$flushed) {
            return;
        }
        if (empty(self::$markers) && empty(self::$counters)) {
            return;
        }
        self::$flushed = true;

        $parts = [];

        foreach (self::$tags as $k => $v) {
            $parts[] = $k . '=' . self::scalarize($v);
        }

        foreach (self::$markers as $name => $data) {
            $ms = (int) round($data['elapsed_ms']);
            $segment = $name . '=' . $ms . 'ms';
            if (!empty($data['detail'])) {
                $segment .= '|' . $data['detail'];
            }
            // Inline matching counter under its marker for readability
            $matchingCounter = $name . '_calls';
            if (isset(self::$counters[$matchingCounter])) {
                $segment .= '|calls=' . self::$counters[$matchingCounter];
                unset(self::$counters[$matchingCounter]);
            }
            $parts[] = $segment;
        }

        // Total wall time of the request (since reset)
        if (self::$requestStart !== null) {
            $total = (int) round((microtime(true) - self::$requestStart) * 1000.0);
            $parts[] = 'total=' . $total . 'ms';
        }

        // Any counters not absorbed above
        foreach (self::$counters as $name => $value) {
            $parts[] = $name . '=' . $value;
        }

        $line = '[LolaCore Perf] ' . implode(' ', $parts);
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Dev-only telemetry; gated by WP_DEBUG. LOLA_PERF
        error_log($line);
    }

    /**
     * Shutdown hook handler. Registered once in lolacore.php so we capture
     * the telemetry even when wp_send_json_*() calls wp_die() and skips
     * the rest of the controller.
     */
    public static function flushIfPending(): void
    {
        self::flush();
    }

    private static function formatDetail(array $detail): string
    {
        $bits = [];
        foreach ($detail as $k => $v) {
            $bits[] = $k . '=' . self::scalarize($v);
        }
        return implode(',', $bits);
    }

    private static function scalarize($value): string
    {
        if (is_bool($value)) {
            return $value ? 'true' : 'false';
        }
        if (is_scalar($value)) {
            return (string) $value;
        }
        if (is_array($value)) {
            return 'arr(' . count($value) . ')';
        }
        return 'obj';
    }
}
