<?php

namespace MintyPHP\Service\Auth;

use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Repository\Auth\ApiTokenRepositoryInterface;
use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Service\Tenant\TenantScopeService;

class ApiTokenService
{
    private const MAX_TOKENS_PER_USER = 10;

    public function __construct(
        private readonly UserReadRepositoryInterface $userReadRepository,
        private readonly ApiTokenRepositoryInterface $apiTokenRepository,
        private readonly AuthSettingsGateway $settingsGateway,
        private readonly TenantScopeService $scopeGateway,
        private readonly DatabaseSessionRepository $databaseSessionRepository,
        private readonly RequestRuntimeInterface $requestRuntime
    ) {
    }

    private function isTokenExpired(?string $expiresAt): bool
    {
        $expiresAt = trim((string) ($expiresAt ?? ''));
        if ($expiresAt === '') {
            return false;
        }
        try {
            $expiry = new \DateTimeImmutable($expiresAt, new \DateTimeZone('UTC'));
            return $expiry <= new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
        } catch (\Exception $e) {
            return true;
        }
    }

    /**
     * Generate a new API token for a user.
     *
     * Returns ['ok' => true, 'token' => 'selector:plaintext', 'id' => int] on success.
     * The plain-text token is shown ONCE and never stored.
     */
    public function create(
        int $userId,
        string $name,
        ?int $tenantId = null,
        ?string $expiresAt = null,
        ?int $createdBy = null
    ): array {
        $name = trim($name);
        if ($name === '') {
            return ['ok' => false, 'error' => 'name_required'];
        }
        if ($userId <= 0) {
            return ['ok' => false, 'error' => 'invalid_user'];
        }

        $user = $this->userReadRepository->find($userId);
        if (!$user || !((int) ($user['active'] ?? 0))) {
            return ['ok' => false, 'error' => 'user_inactive'];
        }

        $activeCount = $this->apiTokenRepository->countActiveForUser($userId);
        if ($activeCount >= self::MAX_TOKENS_PER_USER) {
            return ['ok' => false, 'error' => 'max_tokens_reached'];
        }

        if ($tenantId !== null && $tenantId > 0) {
            $userTenantIds = $this->scopeGateway->getUserTenantIds($userId);
            if (!in_array($tenantId, $userTenantIds, true)) {
                return ['ok' => false, 'error' => 'tenant_not_assigned'];
            }
        }

        $resolvedExpiresAt = $this->resolveExpiresAt($expiresAt);
        if ($resolvedExpiresAt === null) {
            return ['ok' => false, 'error' => 'invalid_expires_at'];
        }

        // Selector is public (used for lookup); verifier is the secret (only its hash is stored).
        $selector = bin2hex(random_bytes(12));
        $plainToken = bin2hex(random_bytes(32));
        $tokenHash = hash('sha256', $plainToken);

        $id = $this->apiTokenRepository->create(
            $userId,
            $name,
            $selector,
            $tokenHash,
            ($tenantId !== null && $tenantId > 0) ? $tenantId : null,
            $resolvedExpiresAt,
            $createdBy
        );

        if (!$id) {
            return ['ok' => false, 'error' => 'create_failed'];
        }

        $createdToken = $this->apiTokenRepository->find($id);
        if (!$createdToken) {
            return ['ok' => false, 'error' => 'create_failed'];
        }

        $fullToken = $selector . ':' . $plainToken;
        return [
            'ok' => true,
            'token' => $fullToken,
            'id' => $id,
            'uuid' => (string) ($createdToken['uuid'] ?? ''),
            'selector' => $selector,
            'expires_at' => $resolvedExpiresAt,
        ];
    }

    public function rotateCurrentToken(int $userId, int $currentTokenId, ?string $expiresAt = null): array
    {
        if ($userId <= 0 || $currentTokenId <= 0) {
            return ['ok' => false, 'error' => 'current_token_not_found'];
        }

        $currentToken = $this->apiTokenRepository->find($currentTokenId);
        if (!$currentToken || (int) ($currentToken['user_id'] ?? 0) !== $userId) {
            return ['ok' => false, 'error' => 'current_token_not_found'];
        }

        if (!empty($currentToken['revoked_at']) || $this->isTokenExpired($currentToken['expires_at'] ?? null)) {
            return ['ok' => false, 'error' => 'current_token_invalid'];
        }

        $activeCountExcludingCurrent = $this->apiTokenRepository->countActiveForUserExcludingId($userId, $currentTokenId);
        if ($activeCountExcludingCurrent >= self::MAX_TOKENS_PER_USER) {
            return ['ok' => false, 'error' => 'max_tokens_reached'];
        }

        $name = trim((string) ($currentToken['name'] ?? ''));
        if ($name === '') {
            $name = 'API token';
        }

        $tenantId = isset($currentToken['tenant_id']) ? (int) ($currentToken['tenant_id']) : 0;
        if ($tenantId <= 0) {
            $tenantId = null;
        }

        // Wrap revoke+create in a transaction so rotation is atomic — no gap where neither token is valid.
        $transactionStarted = false;
        try {
            $this->databaseSessionRepository->beginTransaction();
            $transactionStarted = true;

            if (!$this->apiTokenRepository->revoke($currentTokenId)) {
                $this->databaseSessionRepository->rollbackTransaction();
                return ['ok' => false, 'error' => 'rotate_failed'];
            }

            $created = $this->create($userId, $name, $tenantId, $expiresAt, $userId);
            if (!($created['ok'] ?? false)) {
                $this->databaseSessionRepository->rollbackTransaction();
                $error = (string) ($created['error'] ?? 'rotate_failed');
                if ($error === 'invalid_expires_at') {
                    return ['ok' => false, 'error' => 'invalid_expires_at'];
                }
                if ($error === 'max_tokens_reached') {
                    return ['ok' => false, 'error' => 'max_tokens_reached'];
                }
                return ['ok' => false, 'error' => 'rotate_failed'];
            }

            $this->databaseSessionRepository->commitTransaction();
            $transactionStarted = false;
            $created['tenant_id'] = $tenantId;
            return $created;
        } catch (\Throwable $exception) {
            if ($transactionStarted) {
                try {
                    $this->databaseSessionRepository->rollbackTransaction();
                } catch (\Throwable $rollbackException) {
                    // Ignore rollback error and return rotate_failed below.
                }
            }
            return ['ok' => false, 'error' => 'rotate_failed'];
        }
    }

    private function resolveExpiresAt(?string $expiresAt): ?string
    {
        $nowUtc = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
        $maxTtlDays = $this->settingsGateway->getApiTokenMaxTtlDays();
        $maxExpiryUtc = $nowUtc->modify('+' . $maxTtlDays . ' days');
        $raw = trim((string) ($expiresAt ?? ''));

        if ($raw === '') {
            $defaultDays = $this->settingsGateway->getApiTokenDefaultTtlDays();
            $defaultExpiry = $nowUtc->modify('+' . $defaultDays . ' days');
            if ($defaultExpiry > $maxExpiryUtc) {
                $defaultExpiry = $maxExpiryUtc;
            }
            return $defaultExpiry->format('Y-m-d H:i:s');
        }

        try {
            $parsed = new \DateTimeImmutable($raw);
        } catch (\Exception $exception) {
            return null;
        }

        $expiryUtc = $parsed->setTimezone(new \DateTimeZone('UTC'));
        if ($expiryUtc <= $nowUtc) {
            return null;
        }
        if ($expiryUtc > $maxExpiryUtc) {
            return null;
        }

        return $expiryUtc->format('Y-m-d H:i:s');
    }

    /**
     * Validate a Bearer token string.
     *
     * @return array{user: array, token_record: array, tenant_id: ?int}|null
     */
    public function validate(string $bearerToken): ?array
    {
        if ($bearerToken === '' || strpos($bearerToken, ':') === false) {
            return null;
        }

        [$selector, $plainToken] = explode(':', $bearerToken, 2);
        $selector = trim($selector);
        $plainToken = trim($plainToken);
        if ($selector === '' || $plainToken === '') {
            return null;
        }

        $record = $this->apiTokenRepository->findBySelector($selector);
        if (!$record) {
            // Perform a dummy hash to prevent timing-based selector enumeration.
            $_ = hash('sha256', $plainToken);
            return null;
        }

        if (!empty($record['revoked_at'])) {
            return null;
        }

        if ($this->isTokenExpired($record['expires_at'] ?? null)) {
            return null;
        }

        $storedHash = (string) ($record['token_hash'] ?? '');
        if ($storedHash === '' || !hash_equals($storedHash, hash('sha256', $plainToken))) {
            return null;
        }

        $userId = (int) ($record['user_id'] ?? 0);
        if ($userId <= 0) {
            return null;
        }

        $user = $this->userReadRepository->find($userId);
        if (!$user || !((int) ($user['active'] ?? 0))) {
            return null;
        }

        $ip = $this->requestRuntime->ip();
        $this->apiTokenRepository->updateLastUsed((int) $record['id'], $ip);

        return [
            'user' => $user,
            'token_record' => $record,
            'tenant_id' => !empty($record['tenant_id']) ? (int) $record['tenant_id'] : null,
        ];
    }

    /**
     * Revoke a specific token by ID.
     */
    public function revoke(int $tokenId): array
    {
        $token = $this->apiTokenRepository->find($tokenId);
        if (!$token) {
            return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
        }
        $this->apiTokenRepository->revoke($tokenId);
        return ['ok' => true];
    }

    /**
     * Revoke all active tokens for a user.
     */
    public function revokeAllForUser(int $userId, ?int $tenantId = null): array
    {
        $count = $this->apiTokenRepository->revokeAllForUser($userId, $tenantId);
        return ['ok' => true, 'count' => $count];
    }

    public function revokeAllByAdmin(): int
    {
        return $this->apiTokenRepository->revokeAllActiveByAdmin();
    }

    /**
     * List tokens for a user (for admin UI display -- never exposes hashes).
     */
    public function listForUser(int $userId): array
    {
        return $this->apiTokenRepository->listByUserId($userId);
    }
}
