<?php

declare(strict_types=1);

namespace App\Models;

use App\Support\TokenAbilities;
use Hyperf\Database\Model\Relations\MorphTo;

class PersonalAccessToken extends Model
{
    protected ?string $table = 'personal_access_tokens';

    public bool $incrementing = true;

    protected array $fillable = [
        'tokenable_type',
        'tokenable_id',
        'name',
        'description',
        'token',
        'abilities',
        'allowed_ips',
        'last_used_at',
        'last_used_ip',
        'expires_at',
        'created_by',
        'revoked_by',
        'revoked_at',
    ];

    protected array $casts = [
        'abilities' => 'array',
        'allowed_ips' => 'array',
        'last_used_at' => 'datetime',
        'expires_at' => 'datetime',
        'revoked_at' => 'datetime',
    ];

    protected array $hidden = [
        'token',
    ];

    public function tokenable(): MorphTo
    {
        return $this->morphTo();
    }

    public static function findByPlainToken(string $plain): ?self
    {
        return static::query()->where('token', hash('sha256', $plain))->first();
    }

    public function isExpired(): bool
    {
        return $this->expires_at !== null && $this->expires_at->isPast();
    }

    public function isRevoked(): bool
    {
        return $this->revoked_at !== null;
    }

    public function isActive(): bool
    {
        return ! $this->isExpired() && ! $this->isRevoked();
    }

    public function can(string $ability): bool
    {
        $abilities = $this->abilities ?? [];
        if (in_array(TokenAbilities::WILDCARD, $abilities, true)) {
            return true;
        }
        return in_array($ability, $abilities, true);
    }

    public function ipAllowed(string $ip): bool
    {
        $list = $this->allowed_ips ?? [];
        if (empty($list)) {
            return true;
        }
        foreach ($list as $entry) {
            if ($this->ipMatches($ip, $entry)) {
                return true;
            }
        }
        return false;
    }

    private function ipMatches(string $ip, string $entry): bool
    {
        $entry = trim($entry);
        if ($entry === '') {
            return false;
        }
        if (! str_contains($entry, '/')) {
            return $ip === $entry;
        }
        [$subnet, $bits] = explode('/', $entry, 2);
        $bits = (int) $bits;
        $ipBin = @inet_pton($ip);
        $subnetBin = @inet_pton($subnet);
        if ($ipBin === false || $subnetBin === false || strlen($ipBin) !== strlen($subnetBin)) {
            return false;
        }
        $bytes = intdiv($bits, 8);
        $remainder = $bits % 8;
        if (substr($ipBin, 0, $bytes) !== substr($subnetBin, 0, $bytes)) {
            return false;
        }
        if ($remainder === 0) {
            return true;
        }
        $mask = chr(0xff << (8 - $remainder) & 0xff);
        return (ord($ipBin[$bytes]) & ord($mask)) === (ord($subnetBin[$bytes]) & ord($mask));
    }
}
