<?php
declare(strict_types=1);

namespace App\Auth;

use DateTimeImmutable;
use App\Support\TranslatableException;
use InvalidArgumentException;
use PDO;
use Throwable;

final class ApiTokenService
{
    public const SCOPES = [
        'questions:write','answers:write','comments:write','votes:write','follows:write',
        'answers:accept','reports:write','notifications:read','profile:read',
    ];

    public function __construct(private readonly PDO $pdo) {}

    /** @param list<string> $scopes @return array{token:string,id:int,prefix:string,scopes:list<string>} */
    public function createToken(int $userId, string $label, array $scopes, ?string $expiresAt = null): array
    {
        $this->requireActiveUser($userId);
        $label = trim($label);
        if ((function_exists('mb_strlen') ? mb_strlen($label,'UTF-8') : strlen($label)) > 120) throw new TranslatableException('api_tokens.validation_label_too_long');
        $scopes = array_values(array_unique(array_intersect(array_map('strval',$scopes), self::SCOPES)));
        if ($scopes === []) throw new TranslatableException('api_tokens.validation_scope_required');
        $expiry = null;
        if ($expiresAt !== null && trim($expiresAt) !== '') {
            try { $date = new DateTimeImmutable(trim($expiresAt)); } catch (Throwable) { throw new TranslatableException('api_tokens.validation_expiry_invalid'); }
            if ($date->getTimestamp() <= time()) throw new TranslatableException('api_tokens.validation_expiry_future');
            $expiry = $date->format('Y-m-d H:i:s');
        }
        $prefix = bin2hex(random_bytes(6));
        $secret = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
        $raw = 'qv1_' . $prefix . '_' . $secret;
        $hash = hash('sha256', $raw);
        $statement = $this->pdo->prepare(
            'INSERT INTO api_tokens (user_id,token_prefix,token_hash,device_label,scopes_json,expires_at,created_at) VALUES (:user_id,:prefix,:hash,:label,:scopes,:expires_at,NOW())'
        );
        $statement->execute([
            'user_id'=>$userId,'prefix'=>$prefix,'hash'=>$hash,'label'=>$label===''?null:$label,
            'scopes'=>json_encode($scopes,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'expires_at'=>$expiry,
        ]);
        return ['token'=>$raw,'id'=>(int)$this->pdo->lastInsertId(),'prefix'=>$prefix,'scopes'=>$scopes];
    }

    /** @return array{user:array<string,mixed>,scopes:list<string>,token_id:int}|null */
    public function authenticate(string $rawToken): ?array
    {
        $rawToken = trim($rawToken);
        if ($rawToken === '' || !str_starts_with($rawToken,'qv1_') || strlen($rawToken) > 160) return null;
        $hash = hash('sha256',$rawToken);
        $statement=$this->pdo->prepare(
            "SELECT t.id AS token_id,t.scopes_json,u.id,u.name,u.username,u.email,u.role,u.status,u.points,u.preferred_locale,u.email_verified_at FROM api_tokens t INNER JOIN users u ON u.id=t.user_id WHERE t.token_hash=:hash AND t.revoked_at IS NULL AND (t.expires_at IS NULL OR t.expires_at>NOW()) AND u.status='active' LIMIT 1"
        );
        $statement->execute(['hash'=>$hash]);$row=$statement->fetch(PDO::FETCH_ASSOC);
        if(!is_array($row))return null;
        $scopes=json_decode((string)($row['scopes_json']??'[]'),true);$scopes=is_array($scopes)?array_values(array_intersect(array_map('strval',$scopes),self::SCOPES)):[];
        $this->pdo->prepare('UPDATE api_tokens SET last_used_at=NOW() WHERE id=:id')->execute(['id'=>(int)$row['token_id']]);
        $tokenId=(int)$row['token_id'];unset($row['token_id'],$row['scopes_json']);
        return ['user'=>$row,'scopes'=>$scopes,'token_id'=>$tokenId];
    }

    /** @return list<array<string,mixed>> */
    public function listForUser(int $userId): array
    {
        $statement=$this->pdo->prepare('SELECT id,token_prefix,device_label,scopes_json,last_used_at,expires_at,revoked_at,created_at FROM api_tokens WHERE user_id=:user_id ORDER BY created_at DESC,id DESC');
        $statement->execute(['user_id'=>$userId]);return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    public function revoke(int $userId, int $tokenId): bool
    {
        $statement=$this->pdo->prepare('UPDATE api_tokens SET revoked_at=COALESCE(revoked_at,NOW()) WHERE id=:id AND user_id=:user_id');
        $statement->execute(['id'=>$tokenId,'user_id'=>$userId]);return $statement->rowCount()>0;
    }

    private function requireActiveUser(int $userId): void
    {
        $statement=$this->pdo->prepare("SELECT 1 FROM users WHERE id=:id AND status='active' LIMIT 1");$statement->execute(['id'=>$userId]);
        if($statement->fetchColumn()===false)throw new TranslatableException('auth.active_user_required');
    }
}
