<?php

declare(strict_types=1);

namespace App\Framework\Http\Session;

use App\Framework\DateTime\Clock;
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FileReadException;
use App\Framework\Filesystem\Exceptions\FileWriteException;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\Serializers\JsonSerializer;

/**
 * File-based session storage using the framework's filesystem module
 */
final readonly class FileSessionStorage implements SessionStorage
{
    private FileStorage $storage;

    private JsonSerializer $serializer;

    private FilePath $basePath;

    private Clock $clock;

    public function __construct(?string $path = null, ?Clock $clock = null)
    {
        $this->basePath = FilePath::create($path ?? sys_get_temp_dir() . '/php_sessions');
        $this->storage = new FileStorage($this->basePath->toString());
        $this->serializer = new JsonSerializer();
        $this->clock = $clock ?? new \App\Framework\DateTime\SystemClock();

        $this->ensureDirectoryExists();
    }

    /**
     * @return array<string, mixed>
     */
    public function read(SessionId $sessionId): array
    {
        try {
            $filePath = $this->getSessionFilePath($sessionId);
            $content = $this->storage->get($filePath);

            /** @var array<string, mixed> $data */
            $data = $this->serializer->deserialize($content);

            // @phpstan-ignore function.alreadyNarrowedType
            return is_array($data) ? $data : [];
        } catch (FileNotFoundException) {
            return [];
        } catch (FileReadException) {
            return [];
        } catch (\JsonException) {
            // Handle corrupted JSON gracefully
            return [];
        }
    }

    /**
     * @param array<string, mixed> $data
     */
    public function write(SessionId $sessionId, array $data): void
    {
        try {
            $filePath = $this->getSessionFilePath($sessionId);
            $content = $this->serializer->serialize($data);
            $this->storage->put($filePath, $content);
        } catch (FileWriteException $e) {
            throw new \RuntimeException("Failed to write session data: {$e->getMessage()}", 0, $e);
        }
    }

    public function remove(SessionId $sessionId): void
    {
        try {
            $filePath = $this->getSessionFilePath($sessionId);
            $this->storage->delete($filePath);
        } catch (FileNotFoundException) {
            // File doesn't exist, nothing to remove
        }
    }

    public function migrate(SessionId $oldId, SessionId $newId): void
    {
        try {
            $oldPath = $this->getSessionFilePath($oldId);
            $newPath = $this->getSessionFilePath($newId);

            // Copy old session to new location
            $this->storage->copy($oldPath, $newPath);

            // Remove old session
            $this->storage->delete($oldPath);
        } catch (FileNotFoundException) {
            // Old session doesn't exist, nothing to migrate
        }
    }

    /**
     * Garbage collection - remove expired session files
     */
    public function gc(int $maxLifetime): void
    {
        try {
            $files = $this->storage->listDirectory('.');
            $now = (int) $this->clock->time()->toFloat();

            foreach ($files as $file) {
                if (! str_starts_with($file, 'sess_')) {
                    continue;
                }

                try {
                    $lastModified = $this->storage->lastModified($file);
                    if ($lastModified + $maxLifetime < $now) {
                        $this->storage->delete($file);
                    }
                } catch (FileNotFoundException) {
                    // File was already deleted
                    continue;
                }
            }
        } catch (\Exception) {
            // Ignore GC errors to prevent breaking session functionality
        }
    }

    private function getSessionFilePath(SessionId $sessionId): string
    {
        return 'sess_' . $sessionId->toString();
    }

    private function ensureDirectoryExists(): void
    {
        try {
            if (! $this->basePath->isDirectory()) {
                $this->storage->createDirectory('.', 0700, true);
            }
        } catch (DirectoryCreateException $e) {
            throw new \RuntimeException(
                "Cannot create session directory: {$this->basePath->toString()}. {$e->getMessage()}",
                0,
                $e
            );
        }
    }

    /**
     * Get the base path for sessions
     */
    public function getBasePath(): FilePath
    {
        return $this->basePath;
    }
}
