<?php

namespace App\Services\PublicProfile;

use App\Models\UserModel;
use Config\Database;

/**
 * Owns username assignment/change rules: format validation, uniqueness,
 * and an admin-configurable "max N changes per period" limit (stored in
 * settings group 'profile', keys 'username_change_limit' and
 * 'username_change_period_days' — defaults 3 changes per 30 days if unset).
 */
class UsernameService
{
    private UserModel $userModel;

    public function __construct()
    {
        $this->userModel = new UserModel();
    }

    public function isValidFormat(string $username): bool
    {
        // Lowercase letters, numbers, underscores, hyphens; 3-30 chars;
        // must start with a letter (keeps URLs clean and avoids all-numeric
        // usernames colliding visually with internal IDs).
        return (bool) preg_match('/^[a-z][a-z0-9_-]{2,29}$/', $username);
    }

    public function isAvailable(string $username, ?int $excludeUserId = null): bool
    {
        $builder = $this->userModel->where('username', $username);
        if ($excludeUserId) {
            $builder->where('id !=', $excludeUserId);
        }
        return $builder->first() === null;
    }

    public function suggestFromName(string $name): string
    {
        $base = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $name));
        $base = substr($base, 0, 20) ?: 'user';
        if (! ctype_alpha($base[0] ?? '')) {
            $base = 'u' . $base;
        }

        $candidate = $base;
        $i = 1;
        while (! $this->isAvailable($candidate)) {
            $candidate = $base . (++$i);
        }
        return $candidate;
    }

    /**
     * Sets a username for the first time (no rate limit applies — every
     * user gets one free initial choice at signup).
     */
    public function setInitial(int $userId, string $username): void
    {
        $username = strtolower(trim($username));
        if (! $this->isValidFormat($username)) {
            throw new \InvalidArgumentException('Username must be 3-30 characters, start with a letter, and contain only lowercase letters, numbers, underscores, and hyphens.');
        }
        if (! $this->isAvailable($username, $userId)) {
            throw new \InvalidArgumentException('This username is already taken.');
        }

        $this->userModel->update($userId, ['username' => $username]);
        $this->logChange($userId, null, $username);
    }

    /**
     * Changes an existing username, enforcing the admin-configured rate
     * limit. Throws if the limit has been reached.
     */
    public function change(int $userId, string $newUsername): void
    {
        $newUsername = strtolower(trim($newUsername));
        if (! $this->isValidFormat($newUsername)) {
            throw new \InvalidArgumentException('Username must be 3-30 characters, start with a letter, and contain only lowercase letters, numbers, underscores, and hyphens.');
        }
        if (! $this->isAvailable($newUsername, $userId)) {
            throw new \InvalidArgumentException('This username is already taken.');
        }

        $user = $this->userModel->find($userId);
        if ($user->username === $newUsername) {
            return; // no-op, not a real change
        }

        [$limit, $periodDays] = $this->getLimitConfig();
        $changesInPeriod = $this->changesSince($userId, $periodDays);

        if ($changesInPeriod >= $limit) {
            throw new \RuntimeException(
                "You've reached the limit of {$limit} username change(s) per {$periodDays} days. Please try again later."
            );
        }

        $old = $user->username;
        $this->userModel->update($userId, ['username' => $newUsername]);
        $this->logChange($userId, $old, $newUsername);
    }

    public function remainingChanges(int $userId): array
    {
        [$limit, $periodDays] = $this->getLimitConfig();
        $used = $this->changesSince($userId, $periodDays);
        return [
            'limit'     => $limit,
            'used'      => $used,
            'remaining' => max(0, $limit - $used),
            'period_days' => $periodDays,
        ];
    }

    private function changesSince(int $userId, int $days): int
    {
        return Database::connect()->table('username_change_log')
            ->where('user_id', $userId)
            ->where('old_username IS NOT NULL') // the free initial set doesn't count against the limit
            ->where('created_at >=', date('Y-m-d H:i:s', strtotime("-{$days} days")))
            ->countAllResults();
    }

    private function logChange(int $userId, ?string $old, string $new): void
    {
        Database::connect()->table('username_change_log')->insert([
            'user_id'      => $userId,
            'old_username' => $old,
            'new_username' => $new,
            'created_at'   => date('Y-m-d H:i:s'),
        ]);
    }

    /**
     * @return array{0:int,1:int} [limit, periodDays]
     */
    private function getLimitConfig(): array
    {
        $db = Database::connect();
        $limitRow = $db->table('settings')->where('group', 'profile')->where('key', 'username_change_limit')->get()->getRow();
        $periodRow = $db->table('settings')->where('group', 'profile')->where('key', 'username_change_period_days')->get()->getRow();

        return [
            $limitRow ? (int) $limitRow->value : 3,
            $periodRow ? (int) $periodRow->value : 30,
        ];
    }
}
