<?php

namespace App\Services\Auth;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use App\Models\UserModel;
use Config\Database;

/**
 * Thin wrapper over firebase/php-jwt tuned to this app's token policy:
 * short-lived access tokens (15 min default), rotated refresh tokens
 * (30 days), refresh tokens tracked in api_tokens for revocation.
 */
class JwtService
{
    private ?string $secret;
    private int $accessTtl;
    private int $refreshTtl;

    public function __construct()
    {
        // Nullable on purpose: this service is constructed as part of
        // AuthService (used by both web session-login and API JWT flows).
        // Session-based web login never touches JWT at all, so a missing
        // jwt.secret (e.g. before the installer has run) must not crash
        // mere instantiation — only the actual issue/verify calls below
        // need it, and they check explicitly via requireSecret().
        $this->secret     = env('jwt.secret') ?: null;
        $this->accessTtl  = (int) (env('jwt.accessTokenTTL') ?? 900);
        $this->refreshTtl = (int) (env('jwt.refreshTokenTTL') ?? 2592000);
    }

    private function requireSecret(): string
    {
        if (! $this->secret) {
            throw new \RuntimeException(
                'JWT secret is not configured (jwt.secret in .env). API token issuance is unavailable until this is set.'
            );
        }
        return $this->secret;
    }

    public function issueAccessToken(int $userId, string $roleSlug): string
    {
        $now = time();
        $payload = [
            'sub'  => $userId,
            'role' => $roleSlug,
            'iat'  => $now,
            'exp'  => $now + $this->accessTtl,
            'type' => 'access',
        ];

        return JWT::encode($payload, $this->requireSecret(), 'HS256');
    }

    /**
     * Issues a refresh token AND persists its hash in api_tokens so it can
     * be individually revoked (logout, "log out all devices", password
     * change) — a bare stateless JWT refresh token can't be revoked before
     * its natural expiry, which is unacceptable for the security requirements.
     */
    public function issueRefreshToken(int $userId): string
    {
        $secret = $this->requireSecret();
        $now = time();
        $jti = bin2hex(random_bytes(16));
        $payload = [
            'sub'  => $userId,
            'jti'  => $jti,
            'iat'  => $now,
            'exp'  => $now + $this->refreshTtl,
            'type' => 'refresh',
        ];

        $token = JWT::encode($payload, $secret, 'HS256');

        Database::connect()->table('api_tokens')->insert([
            'user_id'            => $userId,
            'token_hash'         => hash('sha256', $jti), // store hash of jti, not the token itself
            'refresh_token_hash' => hash('sha256', $token),
            'expires_at'         => date('Y-m-d H:i:s', $now + $this->refreshTtl),
            'created_at'         => date('Y-m-d H:i:s'),
        ]);

        return $token;
    }

    public function verifyAccessToken(string $token): ?object
    {
        if (! $this->secret) {
            return null;
        }
        try {
            $payload = JWT::decode($token, new Key($this->secret, 'HS256'));
            if (($payload->type ?? null) !== 'access') {
                return null;
            }
            return $payload;
        } catch (ExpiredException | SignatureInvalidException | \UnexpectedValueException $e) {
            return null;
        }
    }

    public function verifyAndRotateRefreshToken(string $token): ?array
    {
        if (! $this->secret) {
            return null;
        }
        try {
            $payload = JWT::decode($token, new Key($this->secret, 'HS256'));
        } catch (ExpiredException | SignatureInvalidException | \UnexpectedValueException $e) {
            return null;
        }

        if (($payload->type ?? null) !== 'refresh') {
            return null;
        }

        $db = Database::connect();
        $tokenHash = hash('sha256', $token);

        $record = $db->table('api_tokens')
            ->where('refresh_token_hash', $tokenHash)
            ->where('revoked_at', null)
            ->get()->getRow();

        if (! $record) {
            return null; // already revoked / rotated / unknown
        }

        // Rotate: revoke this one, caller issues a fresh pair
        $db->table('api_tokens')->where('id', $record->id)->update(['revoked_at' => date('Y-m-d H:i:s')]);

        $userModel = new UserModel();
        $user = $userModel->findWithRole((int) $payload->sub);

        if (! $user || $user->status !== 'active') {
            return null;
        }

        return ['user' => $user];
    }

    public function revokeAllForUser(int $userId): void
    {
        Database::connect()->table('api_tokens')
            ->where('user_id', $userId)
            ->where('revoked_at', null)
            ->update(['revoked_at' => date('Y-m-d H:i:s')]);
    }
}
