<?php

declare(strict_types=1);

namespace App\Framework\Security;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;

/**
 * Value Object for CSRF token data with creation time and usage tracking
 */
final readonly class CsrfTokenData
{
    public function __construct(
        public CsrfToken $token,
        public Timestamp $createdAt,
        public ?Timestamp $usedAt = null
    ) {
    }

    /**
     * Check if the token has expired based on lifetime
     */
    public function isExpired(Clock $clock, Duration $lifetime): bool
    {
        return $clock->time()->diff($this->createdAt)->greaterThan($lifetime);
    }

    /**
     * Check if the token was used recently within the given window
     */
    public function wasUsedRecently(Clock $clock, Duration $window): bool
    {
        if (! $this->usedAt) {
            return false;
        }

        return $clock->time()->diff($this->usedAt)->lessThan($window);
    }

    /**
     * Check if the token should be kept (not expired or used recently)
     */
    public function shouldKeep(Clock $clock, Duration $lifetime, Duration $usedWindow): bool
    {
        return ! $this->isExpired($clock, $lifetime) || $this->wasUsedRecently($clock, $usedWindow);
    }

    /**
     * Mark the token as used at the current time
     */
    public function markAsUsed(Clock $clock): self
    {
        return new self($this->token, $this->createdAt, $clock->time());
    }

    /**
     * Get the age of the token
     */
    public function getAge(Clock $clock): Duration
    {
        return $clock->time()->diff($this->createdAt);
    }

    /**
     * Check if this token matches the given token string
     */
    public function matches(string $tokenString): bool
    {
        return $this->token->toString() === $tokenString;
    }

    /**
     * Serialize to array for session storage
     * @return array<string, string|float|null>
     */
    public function toArray(): array
    {
        return [
            'token' => $this->token->toString(),
            'created_at' => $this->createdAt->toFloat(),
            'used_at' => $this->usedAt?->toFloat(),
        ];
    }

    /**
     * Deserialize from array (handles both new and legacy formats)
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data, Clock $clock): self
    {
        $token = CsrfToken::fromString($data['token']);

        // Handle Timestamp objects or serialized data
        $createdAtRaw = $data['created_at'];
        if (is_array($createdAtRaw)) {
            // Handle serialized Timestamp object
            $createdAt = Timestamp::fromFloat($createdAtRaw['microTimestamp'] ?? $clock->time()->toFloat());
        } elseif (is_float($createdAtRaw) || is_int($createdAtRaw)) {
            $createdAt = Timestamp::fromFloat((float) $createdAtRaw);
        } elseif ($createdAtRaw instanceof Timestamp) {
            // Already a Timestamp object
            $createdAt = $createdAtRaw;
        } else {
            // Fallback to current time if invalid type
            $createdAt = Timestamp::fromFloat($clock->time()->toFloat());
        }

        $usedAt = null;
        if (isset($data['used_at'])) {
            $usedAtData = $data['used_at'];
            if (is_array($usedAtData)) {
                $usedAt = Timestamp::fromFloat($usedAtData['microTimestamp'] ?? $clock->time()->toFloat());
            } elseif (is_float($usedAtData) || is_int($usedAtData)) {
                $usedAt = Timestamp::fromFloat((float) $usedAtData);
            } elseif ($usedAtData instanceof Timestamp) {
                $usedAt = $usedAtData;
            }
        }

        return new self($token, $createdAt, $usedAt);
    }

    /**
     * Create a new token data instance with current timestamp
     */
    public static function create(CsrfToken $token, Clock $clock): self
    {
        return new self($token, $clock->time());
    }

    /**
     * String representation for debugging
     */
    public function __toString(): string
    {
        $usedAt = $this->usedAt ? $this->usedAt->format('Y-m-d H:i:s') : 'never';

        return sprintf(
            'CsrfTokenData[token=%s, created=%s, used=%s]',
            substr($this->token->toString(), 0, 8) . '...',
            $this->createdAt->format('Y-m-d H:i:s'),
            $usedAt
        );
    }
}
