<?php

declare(strict_types=1);

namespace App\Framework\Database\Exception;

use App\Framework\Database\ValueObjects\SqlState;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\ExceptionContext;

/**
 * Query Execution Exception
 *
 * Thrown when a database query fails to execute due to data errors,
 * runtime errors, or invalid data format.
 *
 * This exception represents errors that occur during query execution
 * (not syntax errors), such as data type mismatches, constraint violations,
 * or data format issues.
 */
final class QueryExecutionException extends DatabaseException
{
    /**
     * Create exception for general query execution failure
     *
     * @param string $query The SQL query that failed
     * @param SqlState $sqlState The SQLSTATE error code
     * @param string|null $errorMessage Optional database error message
     * @param \Throwable|null $previous Previous exception
     */
    public static function forQuery(
        string $query,
        SqlState $sqlState,
        ?string $errorMessage = null,
        ?\Throwable $previous = null
    ): self {
        $message = "Query execution failed: {$errorMessage}";

        $context = ExceptionContext::forOperation('query.execute', 'Database')
            ->withData([
                'query' => self::truncateQuery($query),
                'sqlstate' => $sqlState->code,
                'sqlstate_class' => $sqlState->getClass(),
                'error_category' => self::getSqlStateCategory($sqlState),
            ])
            ->withDebug([
                'full_query' => $query,
                'sqlstate_subclass' => $sqlState->getSubclass(),
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, $previous, 500, $sqlState);
    }

    /**
     * Create exception for data type mismatch
     *
     * @param string $column The column name
     * @param string $expectedType Expected data type
     * @param mixed $actualValue Actual value provided
     * @param SqlState $sqlState The SQLSTATE error code
     */
    public static function forDataTypeMismatch(
        string $column,
        string $expectedType,
        mixed $actualValue,
        SqlState $sqlState
    ): self {
        $actualType = get_debug_type($actualValue);
        $message = "Data type mismatch for column '{$column}': expected {$expectedType}, got {$actualType}";

        $context = ExceptionContext::forOperation('query.data_validation', 'Database')
            ->withData([
                'column' => $column,
                'expected_type' => $expectedType,
                'actual_type' => $actualType,
                'sqlstate' => $sqlState->code,
            ])
            ->withDebug([
                'actual_value' => self::sanitizeValue($actualValue),
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for data truncation
     *
     * @param string $column The column name
     * @param int $maxLength Maximum allowed length
     * @param int $actualLength Actual data length
     * @param SqlState $sqlState The SQLSTATE error code (typically 22001)
     */
    public static function forDataTruncation(
        string $column,
        int $maxLength,
        int $actualLength,
        SqlState $sqlState
    ): self {
        $message = "Data too long for column '{$column}': maximum {$maxLength} characters, got {$actualLength}";

        $context = ExceptionContext::forOperation('query.data_validation', 'Database')
            ->withData([
                'column' => $column,
                'max_length' => $maxLength,
                'actual_length' => $actualLength,
                'sqlstate' => $sqlState->code,
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for numeric value out of range
     *
     * @param string $column The column name
     * @param mixed $value The value that is out of range
     * @param SqlState $sqlState The SQLSTATE error code (typically 22003)
     */
    public static function forNumericOutOfRange(
        string $column,
        mixed $value,
        SqlState $sqlState
    ): self {
        $message = "Numeric value out of range for column '{$column}'";

        $context = ExceptionContext::forOperation('query.data_validation', 'Database')
            ->withData([
                'column' => $column,
                'sqlstate' => $sqlState->code,
            ])
            ->withDebug([
                'value' => self::sanitizeValue($value),
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for invalid datetime format
     *
     * @param string $column The column name
     * @param string $value The invalid datetime value
     * @param SqlState $sqlState The SQLSTATE error code (typically 22007)
     */
    public static function forInvalidDatetime(
        string $column,
        string $value,
        SqlState $sqlState
    ): self {
        $message = "Invalid datetime format for column '{$column}': {$value}";

        $context = ExceptionContext::forOperation('query.data_validation', 'Database')
            ->withData([
                'column' => $column,
                'value' => $value,
                'sqlstate' => $sqlState->code,
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for division by zero
     *
     * @param string $expression The expression that caused division by zero
     * @param SqlState $sqlState The SQLSTATE error code (typically 22012)
     */
    public static function forDivisionByZero(
        string $expression,
        SqlState $sqlState
    ): self {
        $message = "Division by zero error in expression: {$expression}";

        $context = ExceptionContext::forOperation('query.execute', 'Database')
            ->withData([
                'expression' => $expression,
                'sqlstate' => $sqlState->code,
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Create exception for invalid text representation
     *
     * @param string $column The column name
     * @param string $value The invalid value
     * @param SqlState $sqlState The SQLSTATE error code (typically 22P02)
     */
    public static function forInvalidTextRepresentation(
        string $column,
        string $value,
        SqlState $sqlState
    ): self {
        $message = "Invalid text representation for column '{$column}': {$value}";

        $context = ExceptionContext::forOperation('query.data_validation', 'Database')
            ->withData([
                'column' => $column,
                'value' => self::truncateValue($value),
                'sqlstate' => $sqlState->code,
            ])
            ->withDebug([
                'full_value' => $value,
            ]);

        return self::fromContext($message, $context, DatabaseErrorCode::QUERY_EXECUTION_FAILED, null, 500, $sqlState);
    }

    /**
     * Truncate query for safe logging
     *
     * @param string $query The SQL query
     * @param int $maxLength Maximum length
     * @return string Truncated query
     */
    private static function truncateQuery(string $query, int $maxLength = 200): string
    {
        if (strlen($query) <= $maxLength) {
            return $query;
        }

        return substr($query, 0, $maxLength) . '... (truncated)';
    }

    /**
     * Truncate value for safe logging
     *
     * @param string $value The value
     * @param int $maxLength Maximum length
     * @return string Truncated value
     */
    private static function truncateValue(string $value, int $maxLength = 100): string
    {
        if (strlen($value) <= $maxLength) {
            return $value;
        }

        return substr($value, 0, $maxLength) . '... (truncated)';
    }

    /**
     * Sanitize value for logging (prevent logging sensitive data)
     *
     * @param mixed $value The value to sanitize
     * @return string Sanitized value
     */
    private static function sanitizeValue(mixed $value): string
    {
        if (is_array($value)) {
            return '[array:' . count($value) . ']';
        }

        if (is_object($value)) {
            return '[object:' . get_class($value) . ']';
        }

        if (is_string($value) && strlen($value) > 100) {
            return substr($value, 0, 100) . '... (truncated)';
        }

        return (string) $value;
    }

    /**
     * Get human-readable category for SQLSTATE
     *
     * @param SqlState $sqlState The SQLSTATE code
     * @return string Category name
     */
    private static function getSqlStateCategory(SqlState $sqlState): string
    {
        return match ($sqlState->getClass()) {
            '22' => 'Data Exception',
            '23' => 'Integrity Constraint Violation',
            '42' => 'Syntax Error or Access Violation',
            '08' => 'Connection Exception',
            '40' => 'Transaction Rollback',
            default => 'Unknown',
        };
    }
}
