<?php

declare(strict_types=1);

namespace App\Framework\Http\Session;

use App\Application\Security\Events\Session\SessionFingerprintMismatchEvent;
use App\Application\Security\ValueObjects\RequestContext;
use App\Application\Security\ValueObjects\SecurityContext;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\DateTime\Clock;
use App\Framework\Http\Cookies\Cookie;
use App\Framework\Http\Cookies\Cookies;
use App\Framework\Http\HeaderKey;
use App\Framework\Http\Request;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseManipulator;
use App\Framework\Security\CsrfTokenGenerator;

final readonly class SessionManager
{
    private const string SESSION_COOKIE_NAME = 'ms_context';

    public function __construct(
        private SessionIdGenerator $generator,
        private ResponseManipulator $responseManipulator,
        private Clock $clock,
        private CsrfTokenGenerator $csrfTokenGenerator,
        private SessionStorage $storage,
        private SessionCookieConfig $cookieConfig,
        private ?SessionFingerprint $fingerprint = null,
        private ?EventDispatcher $eventDispatcher = null,
    ) {
        // Validiere die Cookie-Konfiguration
        $this->cookieConfig->validate();
    }

    public function getOrCreateSession(Request $request): Session
    {
        // Session-ID aus Cookie lesen
        $sessionId = $this->getSessionIdFromCookies($request->cookies);

        if ($sessionId !== null) {
            // Versuchen, existierende Session zu laden
            $data = $this->storage->read($sessionId);
            error_log("SessionManager: Loading session data for ID " . $sessionId->toString() . ": " . json_encode($data));

            #$session = new Session($sessionId, $this->clock, $this->csrfTokenGenerator);
            #$session->fromArray($data);

            $session = Session::fromArray($sessionId, $this->clock, $this->csrfTokenGenerator, $data);

            if ($session !== null) {
                // Fingerprint validieren wenn aktiviert
                if ($this->fingerprint !== null && ! $this->fingerprint->validate($request, $session)) {
                    // Fingerprint stimmt nicht überein - möglicher Session Hijacking Versuch
                    $this->handleFingerprintMismatch($request, $session);
                    $this->storage->remove($sessionId);

                    return $this->createNewSession($request);
                }

                return $session;
            }
        }

        // Keine gültige Session gefunden - neue erstellen
        return $this->createNewSession($request);
    }

    public function createNewSession(?Request $request = null): Session
    {
        $sessionId = $this->generator->generate();
        $session = Session::fromArray($sessionId, $this->clock, $this->csrfTokenGenerator);

        // Fingerprint speichern wenn Request vorhanden und Fingerprinting aktiviert
        if ($request !== null && $this->fingerprint !== null) {
            $this->fingerprint->store($request, $session);
        }

        return $session;
    }

    public function regenerateSession(Session $session): Session
    {
        $oldData = $session->all();

        $newSession = Session::fromArray($session->id, $this->clock, $this->csrfTokenGenerator, $oldData);

        // Session-Sicherheitsdaten aktualisieren
        $newSession->security->markRegenerated();

        // Alte Session löschen
        $this->storage->remove($session->id);

        // Neue Session speichern
        $this->storage->write($newSession->id, $newSession->all());

        return $newSession;
    }

    public function saveSession(Session $session, Response $response): Response
    {
        $this->saveSessionData($session);

        // Setze das Session-Cookie
        $cookieName = $this->cookieConfig->getCookieName(self::SESSION_COOKIE_NAME);
        $cookie = new Cookie(
            $cookieName,
            $session->id->toString(),
            $this->cookieConfig->lifetime,
            $this->cookieConfig->path,
            $this->cookieConfig->domain,
            $this->cookieConfig->secure,
            $this->cookieConfig->httpOnly,
            $this->cookieConfig->sameSite
        );

        return $this->responseManipulator->withHeader(
            $response,
            HeaderKey::SET_COOKIE,
            $cookie->toHeaderString()
        );
    }

    /**
     * Speichert nur die Session-Daten ohne Cookie zu setzen
     */
    private function saveSessionData(Session $session): void
    {
        $data = $session->all();
        error_log("SessionManager: Saving session data for ID " . $session->id->toString() . ": " . json_encode($data));
        $this->storage->write($session->id, $data);
    }

    private function getSessionIdFromCookies(Cookies $cookies): ?SessionId
    {
        $cookieName = $this->cookieConfig->getCookieName(self::SESSION_COOKIE_NAME);
        $sessionCookie = $cookies->get($cookieName);

        if ($sessionCookie === null) {
            return null;
        }

        return SessionId::fromString($sessionCookie->value);
    }

    public function destroySession(Session $session, Response $response): Response
    {
        // Session löschen
        $this->storage->remove($session->id);

        // Cookie zum Löschen setzen (abgelaufenes Datum)
        $cookieName = $this->cookieConfig->getCookieName(self::SESSION_COOKIE_NAME);
        $cookie = new Cookie(
            $cookieName,
            '',
            1, // In der Vergangenheit
            $this->cookieConfig->path,
            $this->cookieConfig->domain,
            $this->cookieConfig->secure,
            $this->cookieConfig->httpOnly,
            $this->cookieConfig->sameSite
        );

        // Cookie dem Response hinzufügen
        return $this->responseManipulator->withHeader($response, HeaderKey::SET_COOKIE, $cookie->toHeaderString());
    }

    public function getCookieName(): string
    {
        return $this->cookieConfig->getCookieName(self::SESSION_COOKIE_NAME);
    }

    public function getCookieConfig(): SessionCookieConfig
    {
        return $this->cookieConfig;
    }

    private function handleFingerprintMismatch(Request $request, Session $session): void
    {
        if ($this->eventDispatcher === null || $this->fingerprint === null) {
            return;
        }

        $mismatchData = $this->fingerprint->analyzeMismatch($request, $session);

        $event = new SessionFingerprintMismatchEvent(
            sessionId: $session->id->toString(),
            expectedFingerprint: $mismatchData->storedHash,
            actualFingerprint: $mismatchData->currentHash,
            mismatchedComponents: $mismatchData->mismatchedComponents,
            requestContext: RequestContext::fromGlobals(),
            securityContext: SecurityContext::create(
                sourceIp: $request->server->getClientIp()->value,
                #authenticationMethod: $session->get('auth_method', 'session'),
                userAgent: $request->server->getUserAgent()->value,
                sessionId: $session->id->toString(),
                requestId: $request->id->toString(),
                userId: $session->get('user_id'),
            )
        );

        $this->eventDispatcher->dispatch($event);
    }
}
