<?php

class CfSetup
{
    public const SESSION_ID_LENGTH = 64;
    public const MAX_ATTEMPTS = 5;
    public const SETUP_SYSTEM_KEY = 'is_setup_complete';
    public const FALSE_STRING = 'false';

    public function __construct(
        public readonly PDO $db
    ) {

    }

    /**
     * @return bool
     */
    public function isSetupComplete(): bool
    {
        $stmt = $this->db->prepare("SELECT value FROM system WHERE key = :key");
        $stmt->execute(['key' => self::SETUP_SYSTEM_KEY]);
        return $stmt->fetchColumn() !== self::FALSE_STRING;
    }

    /**
     * @return void
     */
    public function completeSetup(): void
    {
        $this->db->prepare("UPDATE system SET value = 'true' WHERE key = :key")->execute(['key' => self::SETUP_SYSTEM_KEY]);
    }

    /**
     * @return int|null
     */
    private function activeCodeId(): ?int
    {
        $stmt = $this->db->prepare("
                SELECT id
                FROM setup_codes
                WHERE is_revoked = false
                  AND session_id IS NULL
                  AND expires_at >= NOW()
                  AND attempts < :attempts
                  ");
        $stmt->execute(['attempts' => self::MAX_ATTEMPTS]);
        return $stmt->fetchColumn();
    }

    /**
     * @param int $id
     * @return void
     */
    private function incrementAttempts(int $id): void
    {
        $stmt = $this->db->prepare("UPDATE setup_codes SET attempts = attempts + 1 WHERE id = :id");
        $stmt->execute(['id' => $id]);
    }

    /**
     * @param int $codeId
     * @param string $sessionId
     * @return void
     */
    private function updateSessionId(int $codeId, string $sessionId): void
    {
        $stmt = $this->db->prepare("
                    UPDATE setup_codes
                    SET session_id = :session_id,
                        is_used = true
                    WHERE id = :id
                ");
        $stmt->bindParam(':session_id', $sessionId, PDO::PARAM_STR);
        $stmt->bindParam(':id', $codeId, PDO::PARAM_INT);
        $stmt->execute();
    }

    /**
     * @param string $code
     * @return array
     * @throws Exception
     */
    public function validateCode(string $code): array
    {
        try {
            $this->db->beginTransaction();

            if ($this->isSetupComplete()) {
                $this->db->commit();
                return [
                    'isValid' => false,
                    'sessionId' => null,
                    'error' => 'Setup is already complete'
                ];
            }

            $stmt = $this->db->prepare("
                SELECT id
                FROM setup_codes
                WHERE code = :code
                  AND is_revoked = false
                  AND is_used = false
                  AND session_id IS NULL
                  AND expires_at >= NOW()
                  AND attempts < :attempts");

            $stmt->execute(['attempts' => self::MAX_ATTEMPTS, 'code' => $code]);
            $codeId = $stmt->fetchColumn();

            if (!$codeId) {
                $activeCodeId = $this->activeCodeId();
                if ($activeCodeId) {
                    $this->incrementAttempts($activeCodeId);
                }

                $this->db->commit();
                return [
                    'isValid' => false,
                    'sessionId' => null,
                    'error' => 'Incorrect or expired code.'
                ];
            }

            $sessionId = bin2hex(random_bytes(self::SESSION_ID_LENGTH / 2));
            $this->updateSessionId($codeId, $sessionId);

            $this->db->commit();

            return [
                'isValid' => true,
                'sessionId' => $sessionId,
                'error' => null
            ];
        } catch (PDOException $e) {
            $this->db->rollBack();
            syslog(LOG_ERR, "Failed to validate setup code: " . $e->getMessage());
            throw new \Exception('Error occurred during setup code validation.');
        }
    }

    /**
     * @param string|null $sessionId
     * @return bool
     */
    public function isSessionValid(?string $sessionId): bool
    {
        $stmt = $this->db->prepare("
                SELECT id
                FROM setup_codes
                WHERE is_revoked = false
                  AND session_id = :sessionId
                  AND expires_at >= NOW()
                  ");
        $stmt->execute(['sessionId' => $sessionId]);
        return $stmt->fetchColumn() !== false;
    }
}
