<?php

declare(strict_types=1);

namespace App\Framework\Database\Validation;

use InvalidArgumentException;

/**
 * Validator for database identifiers (tables, columns, indexes)
 *
 * Provides centralized validation logic for:
 * - Name format validation
 * - SQL injection prevention
 * - Reserved keyword detection
 * - Length constraints
 *
 * Follows SQL standards for identifier naming:
 * - Must start with letter or underscore
 * - Can contain letters, numbers, underscores
 * - Maximum 64 characters (MySQL/PostgreSQL standard)
 */
final readonly class DatabaseIdentifierValidator
{
    private const MAX_LENGTH = 64; // MySQL/PostgreSQL standard

    private const SQL_KEYWORDS = [
        'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER',
        'TABLE', 'INDEX', 'VIEW', 'DATABASE', 'SCHEMA', 'FROM', 'WHERE',
        'JOIN', 'UNION', 'GROUP', 'ORDER', 'HAVING', 'LIMIT', 'OFFSET',
        'AND', 'OR', 'NOT', 'IN', 'EXISTS', 'LIKE', 'BETWEEN', 'NULL',
        'PRIMARY', 'FOREIGN', 'KEY', 'UNIQUE', 'CHECK', 'DEFAULT',
    ];

    /**
     * Validate database identifier
     *
     * @param string $identifier The identifier to validate
     * @param string $type Type of identifier (for error messages: 'table', 'column', 'index')
     * @param array<string> $allowedValues Special values that bypass normal validation (e.g., 'PRIMARY')
     * @throws InvalidArgumentException if validation fails
     */
    public function validate(
        string $identifier,
        string $type = 'identifier',
        array $allowedValues = []
    ): void {
        // Check if it's an allowed special value
        if (in_array(strtoupper($identifier), $allowedValues, true)) {
            return;
        }

        $this->validateNotEmpty($identifier, $type);
        $this->validateLength($identifier, $type);
        $this->validateStartsWithLetterOrUnderscore($identifier, $type);
        $this->validateFormat($identifier, $type);
        $this->validateNoSqlInjection($identifier, $type);
    }

    /**
     * Check if identifier is empty
     */
    private function validateNotEmpty(string $identifier, string $type): void
    {
        if (empty($identifier)) {
            throw new InvalidArgumentException(
                ucfirst($type) . " name cannot be empty"
            );
        }
    }

    /**
     * Validate identifier length
     */
    private function validateLength(string $identifier, string $type): void
    {
        if (strlen($identifier) > self::MAX_LENGTH) {
            throw new InvalidArgumentException(
                ucfirst($type) . " name exceeds maximum length of " . self::MAX_LENGTH
                . " characters: {$identifier}"
            );
        }
    }

    /**
     * Validate identifier starts with letter or underscore
     */
    private function validateStartsWithLetterOrUnderscore(string $identifier, string $type): void
    {
        if (!preg_match('/^[a-zA-Z_]/', $identifier)) {
            throw new InvalidArgumentException(
                ucfirst($type) . " name must start with a letter or underscore: {$identifier}"
            );
        }
    }

    /**
     * Validate identifier format (alphanumeric + underscores only)
     */
    private function validateFormat(string $identifier, string $type): void
    {
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $identifier)) {
            throw new InvalidArgumentException(
                ucfirst($type) . " name can only contain letters, numbers, and underscores: {$identifier}"
            );
        }
    }

    /**
     * Validate no SQL injection patterns
     */
    private function validateNoSqlInjection(string $identifier, string $type): void
    {
        if ($this->containsSuspiciousPatterns($identifier)) {
            throw new InvalidArgumentException(
                ucfirst($type) . " name contains suspicious SQL patterns: {$identifier}"
            );
        }
    }

    /**
     * Check if identifier is a reserved SQL keyword
     */
    public function isReservedKeyword(string $identifier): bool
    {
        return in_array(strtoupper($identifier), self::SQL_KEYWORDS, true);
    }

    /**
     * Get maximum allowed length for identifiers
     */
    public function getMaxLength(): int
    {
        return self::MAX_LENGTH;
    }

    /**
     * Detect suspicious SQL injection patterns
     *
     * This checks for SQL metacharacters that would be caught by format validation
     * if present in an identifier. Since valid identifiers can only contain
     * alphanumeric characters and underscores, any SQL injection attempt would
     * contain invalid characters and fail format validation first.
     *
     * We keep this as an additional security layer for explicit SQL patterns.
     */
    private function containsSuspiciousPatterns(string $value): bool
    {
        // Note: Format validation already prevents most SQL injection:
        // - No quotes (', ")
        // - No semicolons (;)
        // - No hyphens (--)
        // - No slashes (/, \)
        // - No parentheses, spaces, operators, etc.
        //
        // This method catches edge cases and provides defense in depth.

        $suspiciousPatterns = [
            '--',    // SQL comment (also fails format validation)
            '/*',    // SQL comment start (also fails format validation)
            '*/',    // SQL comment end (also fails format validation)
            ';',     // Statement separator (also fails format validation)
            '\'',    // Single quote (also fails format validation)
            '"',     // Double quote (also fails format validation)
            '\\',    // Backslash escape (also fails format validation)
        ];

        foreach ($suspiciousPatterns as $pattern) {
            if (str_contains($value, $pattern)) {
                return true;
            }
        }

        return false;
    }
}
