<?php

namespace App\View\Engines;

use Illuminate\View\Engines\CompilerEngine;
use Illuminate\View\ViewException;
use Illuminate\Support\Str;
use Throwable;
use App\Performance\Profiling;

class ProfiledCompilerEngine extends CompilerEngine
{
    private static function isTransientCompiledViewStatFailure(Throwable $e): bool
    {
        $msg = $e->getMessage();

        return str_contains($msg, 'filemtime(): stat failed')
            || str_contains($msg, 'stat failed for')
            || str_contains($msg, 'failed to open stream');
    }

    private static function shortenViewPath(string $path): string
    {
        $p = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path);

        $needle = DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR;
        $pos = strpos($p, $needle);
        if ($pos !== false) {
            $rel = substr($p, $pos + strlen($needle));
            return str_replace(DIRECTORY_SEPARATOR, '/', $rel);
        }

        $parts = array_values(array_filter(explode(DIRECTORY_SEPARATOR, $p)));
        $tail = array_slice($parts, -4);

        return implode('/', $tail);
    }

    /**
     * Get the evaluated contents of the view.
     *
     * This is a lightly-instrumented copy of the framework's CompilerEngine::get
     * so we can separate Blade compile time vs PHP evaluation time.
     */
    public function get($path, array $data = [])
    {
        $enabled = Profiling::enabled();

        if (! $enabled) {
            try {
                return parent::get($path, $data);
            } catch (Throwable $e) {
                if (! self::isTransientCompiledViewStatFailure($e)) {
                    throw $e;
                }

                // Same Windows transient as the profiled path: compiled view can disappear between
                // `exists()` and `filemtime()`. Force a recompile and retry once.
                $compiledPath = $this->compiler->getCompiledPath($path);
                clearstatcache(true, $compiledPath);

                $this->compiler->compile($path);

                return $this->evaluatePath($compiledPath, $data);
            }
        }

        $stackFrame = null;
        $stackIndex = null;

        try {
            $req = request();
            $stack = (array) $req->attributes->get('perf.blade.stack', []);
            $stackIndex = count($stack);
            $stackFrame = [
                'path' => (string) $path,
                'start' => microtime(true),
                'child_ms' => 0.0,
            ];
            $stack[] = $stackFrame;
            $req->attributes->set('perf.blade.stack', $stack);
        } catch (\Throwable $e) {
            $stackFrame = null;
            $stackIndex = null;
        }

        $this->lastCompiled[] = $path;

        $compileMs = 0.0;
        $evalMs = 0.0;
        $compiled = 0;

        if (! isset($this->compiledOrNotExpired[$path])) {
            $expired = false;

            // On Windows (and sometimes under AV / file-lock contention), the compiled view file can
            // disappear between the framework's `exists()` check and `filemtime()`, turning a warning
            // into a fatal ErrorException. Treat that scenario as "expired" and recompile.
            try {
                $expired = $this->compiler->isExpired($path);
            } catch (Throwable $e) {
                if (! self::isTransientCompiledViewStatFailure($e)) {
                    throw $e;
                }

                $compiledPath = $this->compiler->getCompiledPath($path);
                clearstatcache(true, $compiledPath);

                // Retry once after clearing stat cache; if it still fails, force a recompile.
                try {
                    $expired = $this->compiler->isExpired($path);
                } catch (Throwable $e2) {
                    if (! self::isTransientCompiledViewStatFailure($e2)) {
                        throw $e2;
                    }
                    $expired = true;
                }
            }

            if ($expired) {
                $t = microtime(true);
                $this->compiler->compile($path);
                $compileMs = (microtime(true) - $t) * 1000;
                $compiled = 1;
            }
        }

        try {
            $t = microtime(true);
            $results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
            $evalMs = (microtime(true) - $t) * 1000;
        } catch (ViewException $e) {
            if (! Str::of($e->getMessage())->contains(['No such file or directory', 'File does not exist at path'])) {
                throw $e;
            }

            if (! isset($this->compiledOrNotExpired[$path])) {
                throw $e;
            }

            $t = microtime(true);
            $this->compiler->compile($path);
            $compileMs += (microtime(true) - $t) * 1000;
            $compiled = 1;

            $t = microtime(true);
            $results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
            $evalMs = (microtime(true) - $t) * 1000;
        } catch (Throwable $e) {
            if (! self::isTransientCompiledViewStatFailure($e) || isset($this->compiledOrNotExpired[$path])) {
                throw $e;
            }

            // Same transient case as above, but thrown while evaluating (rare). Force a recompile and retry.
            $compiledPath = $this->compiler->getCompiledPath($path);
            clearstatcache(true, $compiledPath);

            $t = microtime(true);
            $this->compiler->compile($path);
            $compileMs += (microtime(true) - $t) * 1000;
            $compiled = 1;

            $t = microtime(true);
            $results = $this->evaluatePath($compiledPath, $data);
            $evalMs = (microtime(true) - $t) * 1000;
        }

        $this->compiledOrNotExpired[$path] = true;
        array_pop($this->lastCompiled);

        $totalMs = null;
        $selfMs = null;

        // Compute exclusive (self) time by subtracting nested child time from this view's total time.
        try {
            $req = request();
            $stack = (array) $req->attributes->get('perf.blade.stack', []);

            if ($stackIndex !== null && isset($stack[$stackIndex])) {
                $frame = $stack[$stackIndex];
                $totalMs = (microtime(true) - (float) ($frame['start'] ?? microtime(true))) * 1000;
                $childMs = (float) ($frame['child_ms'] ?? 0.0);
                $selfMs = max(0.0, $totalMs - $childMs);

                // Pop current frame.
                array_pop($stack);

                // Attribute this view's TOTAL time to its parent as child time.
                $parentIndex = count($stack) - 1;
                if ($parentIndex >= 0 && isset($stack[$parentIndex])) {
                    $stack[$parentIndex]['child_ms'] = (float) ($stack[$parentIndex]['child_ms'] ?? 0.0) + $totalMs;
                }

                $req->attributes->set('perf.blade.stack', $stack);
            }
        } catch (\Throwable $e) {
            // ignore
        }

        try {
            $req = request();

            $req->attributes->set('perf.blade.views_count', (int) $req->attributes->get('perf.blade.views_count', 0) + 1);
            $req->attributes->set('perf.blade.compiled_count', (int) $req->attributes->get('perf.blade.compiled_count', 0) + $compiled);

            $req->attributes->set('perf.blade.compile_ms', (float) $req->attributes->get('perf.blade.compile_ms', 0.0) + $compileMs);
            $req->attributes->set('perf.blade.eval_ms', (float) $req->attributes->get('perf.blade.eval_ms', 0.0) + $evalMs);

            if ($totalMs !== null) {
                $req->attributes->set('perf.blade.total_ms_sum', (float) $req->attributes->get('perf.blade.total_ms_sum', 0.0) + (float) $totalMs);

                $totalByView = $req->attributes->get('perf.blade.total_by_view', []);
                if (!is_array($totalByView)) {
                    $totalByView = [];
                }
                if (count($totalByView) < 250) {
                    $viewKey = self::shortenViewPath((string) $path);
                    $totalByView[$viewKey] = (float) ($totalByView[$viewKey] ?? 0.0) + (float) $totalMs;
                    $req->attributes->set('perf.blade.total_by_view', $totalByView);
                }

                $maxTotal = (float) $req->attributes->get('perf.blade.max_total_ms', 0.0);
                if ($totalMs > $maxTotal) {
                    $req->attributes->set('perf.blade.max_total_ms', (float) $totalMs);
                    $req->attributes->set('perf.blade.max_total_view', basename((string) $path));
                }
            }

            if ($selfMs !== null) {
                $req->attributes->set('perf.blade.self_ms_sum', (float) $req->attributes->get('perf.blade.self_ms_sum', 0.0) + (float) $selfMs);

                $selfByView = $req->attributes->get('perf.blade.self_by_view', []);
                if (!is_array($selfByView)) {
                    $selfByView = [];
                }
                if (count($selfByView) < 250) {
                    $viewKey = self::shortenViewPath((string) $path);
                    $selfByView[$viewKey] = (float) ($selfByView[$viewKey] ?? 0.0) + (float) $selfMs;
                    $req->attributes->set('perf.blade.self_by_view', $selfByView);
                }

                $maxSelf = (float) $req->attributes->get('perf.blade.max_self_ms', 0.0);
                if ($selfMs > $maxSelf) {
                    $req->attributes->set('perf.blade.max_self_ms', (float) $selfMs);
                    $req->attributes->set('perf.blade.max_self_view', basename((string) $path));
                }
            }

            $maxEval = (float) $req->attributes->get('perf.blade.max_eval_ms', 0.0);
            if ($evalMs > $maxEval) {
                $req->attributes->set('perf.blade.max_eval_ms', $evalMs);
                $req->attributes->set('perf.blade.max_eval_view', basename((string) $path));
            }

            $maxCompile = (float) $req->attributes->get('perf.blade.max_compile_ms', 0.0);
            if ($compileMs > $maxCompile) {
                $req->attributes->set('perf.blade.max_compile_ms', $compileMs);
                $req->attributes->set('perf.blade.max_compile_view', basename((string) $path));
            }
        } catch (\Throwable $e) {
            // Never let profiling break rendering.
        }

        return $results;
    }
}
