<?php

namespace App\Traits;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

/**
 * SealsAdminAttributes — defense-in-depth audit guard against client-area
 * mass assignment of admin-only columns.
 *
 * Used by: ClientProduct (PRC-182), Invoice (PRC-195), Order (PRC-198),
 * ClientBalance (PRC-201), Admin (PRC-216).
 *
 * ## Why this exists
 *
 * The audit findings (SEC-Group-3) flag a *risk*: an inattentive developer
 * wires up a client-area endpoint with `$model->fill($request->validated())`
 * or `Model::create($request->all())` and a customer mutates an admin-
 * controlled column (`expire_date`, `total`, `payment_status`, `admin_id`,
 * `is_ai_staff`, `email_verified`, …).
 *
 * No such endpoint exists today — current checkout, client-area, and store
 * controllers hard-code the sealed values they pass to `::create()`. But
 * Eloquent's mass-assignment model trusts `$fillable` only, and the admin
 * code legitimately needs to fill every sealed column, so we cannot remove
 * them from `$fillable`.
 *
 * ## What this trait does
 *
 * Each model declares a static `$sealedAdminAttributes` array. On every
 * `fill()`, if any sealed key is present AND we are not in a trusted context
 * (admin guard, console/queue, or an explicit unsealed block), we emit a
 * warning log with the route, URL, and caller stack snippet. We do NOT strip
 * the keys — stripping would break the existing trusted code paths that pass
 * sealed attributes via hard-coded arrays.
 *
 * The log entry is the audit signal. A future endpoint that accidentally
 * passes user input to a sealed field will produce a `WARNING` in the logs
 * every request, making the misuse trivially detectable. CI / log
 * monitoring should treat these warnings as security incidents.
 *
 * ## Opting in to a trusted block (for legitimate non-admin code paths)
 *
 * Checkout, billing services, and other server-side flows that legitimately
 * set sealed attributes from a non-admin context can wrap the call in
 * `Model::unsealed(fn () => Model::create([...]))` to silence the audit log
 * for that operation. This is a soft trust marker — the keys still pass
 * through normally; we just don't log.
 *
 * ## Trusted contexts (no warning emitted)
 *
 * - CLI commands, queue workers, scheduled jobs (`app()->runningInConsole()`)
 * - Admin web/Sanctum guards (`Auth::guard('admin')->check()`)
 * - Explicit `Model::unsealed(...)` block
 */
trait SealsAdminAttributes
{
    /**
     * Process-local flag set by Model::unsealed(). When true, the next fill()
     * skips the warning emission (still applies the assignment).
     */
    protected static bool $sealingSuspended = false;

    /**
     * Re-entrancy guard. Auth::guard('admin')->check() loads the Admin model,
     * which uses this trait — calling Auth::check() from fill() would recurse
     * infinitely and segfault the worker. While we're resolving auth, treat
     * the context as trusted (the only path that hits this from inside auth
     * is the framework hydrating the admin user itself).
     */
    protected static bool $resolvingAuth = false;

    /**
     * Override Model::fill() to emit a warning when sealed attributes are
     * present outside a trusted context. Never modifies the attribute array.
     */
    public function fill(array $attributes)
    {
        if (! static::$sealingSuspended
            && ! empty(static::$sealedAdminAttributes)
            && ! $this->isTrustedSealingContext()
        ) {
            $hits = array_values(array_intersect(
                array_keys($attributes),
                static::$sealedAdminAttributes
            ));

            if (! empty($hits)) {
                Log::warning('SealsAdminAttributes: admin-only keys filled from non-admin context', [
                    'model' => static::class,
                    'sealed_keys' => $hits,
                    'route' => optional(request()->route())->getName(),
                    'url' => request()->fullUrl(),
                    'caller' => $this->resolveCallerSnippet(),
                    'guard' => $this->resolveActiveGuardName(),
                ]);
            }
        }

        return parent::fill($attributes);
    }

    /**
     * Run a closure with the audit log suspended. Use this in legitimate
     * non-admin server-side code paths (checkout, billing service, etc.)
     * that hard-code sealed attribute values.
     *
     *     ClientProduct::unsealed(fn () => ClientProduct::create([
     *         'client_id' => $client->id,
     *         'product_id' => $product->id,
     *         // ...
     *     ]));
     *
     * @template TReturn
     * @param callable(): TReturn $callback
     * @return TReturn
     */
    public static function unsealed(callable $callback)
    {
        $previous = static::$sealingSuspended;
        static::$sealingSuspended = true;
        try {
            return $callback();
        } finally {
            static::$sealingSuspended = $previous;
        }
    }

    /**
     * Trust the call if running in CLI/queue, or if the admin guard is
     * authenticated. Everything else still applies the assignment, but emits
     * an audit warning so future client-area misuse is visible.
     */
    protected function isTrustedSealingContext(): bool
    {
        if (app()->runningInConsole()) {
            return true;
        }

        // Re-entrancy: Auth::guard('admin')->check() hydrates the Admin model,
        // which uses this trait. Calling Auth::check() from inside fill() would
        // recurse and segfault the PHP worker. Treat the inner call as trusted.
        if (static::$resolvingAuth) {
            return true;
        }

        try {
            static::$resolvingAuth = true;
            if (Auth::guard('admin')->check()) {
                return true;
            }
        } catch (\Throwable $e) {
            // Auth resolution may fail during early bootstrap; fall through.
        } finally {
            static::$resolvingAuth = false;
        }

        return false;
    }

    /**
     * Best-effort guard label for the log entry. Useful for spotting
     * "this was a `client` guard call mutating sealed columns" patterns.
     */
    protected function resolveActiveGuardName(): string
    {
        // Same re-entrancy concern as isTrustedSealingContext().
        if (static::$resolvingAuth) {
            return 'unknown';
        }

        try {
            static::$resolvingAuth = true;
            foreach (['admin', 'client', 'web'] as $name) {
                if (Auth::guard($name)->check()) {
                    return $name;
                }
            }
        } catch (\Throwable $e) {
            // Ignore guard resolution issues.
        } finally {
            static::$resolvingAuth = false;
        }

        return 'guest';
    }

    /**
     * Short, log-friendly snippet of the call stack so the offending caller
     * can be located without dumping the whole trace.
     *
     * @return array<int, string>
     */
    protected function resolveCallerSnippet(): array
    {
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 12);
        $snippet = [];
        foreach (array_slice($trace, 2) as $frame) {
            $file = $frame['file'] ?? '?';
            $line = $frame['line'] ?? 0;
            if (str_contains($file, '/vendor/')) {
                continue;
            }
            $snippet[] = basename($file) . ':' . $line;
            if (count($snippet) >= 4) {
                break;
            }
        }

        return $snippet;
    }
}
