<?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;

/**
 * Constraint Violation Exception
 *
 * Thrown when a database integrity constraint is violated.
 *
 * This exception represents SQLSTATE class 23 (Integrity Constraint Violation) errors:
 * - 23000: Generic integrity constraint violation
 * - 23502: Not null violation
 * - 23503: Foreign key violation
 * - 23505: Unique constraint violation
 * - 23514: Check constraint violation
 */
final class ConstraintViolationException extends DatabaseException
{
    /**
     * Create exception for generic constraint violation
     *
     * @param string $constraintName Name of violated constraint
     * @param SqlState $sqlState The SQLSTATE error code
     * @param string|null $errorMessage Optional database error message
     * @param \Throwable|null $previous Previous exception
     */
    public static function constraintViolated(
        string $constraintName,
        SqlState $sqlState,
        ?string $errorMessage = null,
        ?\Throwable $previous = null
    ): self {
        $message = "Integrity constraint '{$constraintName}' violated";

        if ($errorMessage !== null) {
            $message .= ": {$errorMessage}";
        }

        $context = ExceptionContext::forOperation('constraint.validate', 'Database')
            ->withData([
                'constraint_name' => $constraintName,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
            ]);

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

    /**
     * Create exception for unique constraint violation
     *
     * @param string $table Table name
     * @param string $column Column name or constraint name
     * @param mixed $value The duplicate value
     * @param SqlState $sqlState The SQLSTATE error code (typically 23505)
     */
    public static function uniqueViolation(
        string $table,
        string $column,
        mixed $value,
        SqlState $sqlState
    ): self {
        $message = "Unique constraint violation on '{$table}.{$column}': value already exists";

        $context = ExceptionContext::forOperation('constraint.unique', 'Database')
            ->withData([
                'table' => $table,
                'column' => $column,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'unique',
            ])
            ->withDebug([
                'duplicate_value' => self::sanitizeValue($value),
            ]);

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

    /**
     * Create exception for foreign key violation
     *
     * @param string $table Table name
     * @param string $foreignKey Foreign key column
     * @param string $referencedTable Referenced table name
     * @param mixed $value The violating value
     * @param SqlState $sqlState The SQLSTATE error code (typically 23503)
     */
    public static function foreignKeyViolation(
        string $table,
        string $foreignKey,
        string $referencedTable,
        mixed $value,
        SqlState $sqlState
    ): self {
        $message = "Foreign key constraint violation on '{$table}.{$foreignKey}': " .
                   "referenced record in '{$referencedTable}' does not exist";

        $context = ExceptionContext::forOperation('constraint.foreign_key', 'Database')
            ->withData([
                'table' => $table,
                'foreign_key' => $foreignKey,
                'referenced_table' => $referencedTable,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'foreign_key',
            ])
            ->withDebug([
                'violating_value' => self::sanitizeValue($value),
            ]);

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

    /**
     * Create exception for foreign key violation on delete
     *
     * @param string $table Table name
     * @param mixed $deletedId ID being deleted
     * @param string $referencingTable Table that references the record
     * @param SqlState $sqlState The SQLSTATE error code (typically 23503)
     */
    public static function foreignKeyViolationOnDelete(
        string $table,
        mixed $deletedId,
        string $referencingTable,
        SqlState $sqlState
    ): self {
        $message = "Cannot delete record from '{$table}': " .
                   "referenced by records in '{$referencingTable}'";

        $context = ExceptionContext::forOperation('constraint.foreign_key_delete', 'Database')
            ->withData([
                'table' => $table,
                'referencing_table' => $referencingTable,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'foreign_key',
                'operation' => 'delete',
            ])
            ->withDebug([
                'deleted_id' => self::sanitizeValue($deletedId),
            ]);

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

    /**
     * Create exception for NOT NULL violation
     *
     * @param string $table Table name
     * @param string $column Column name
     * @param SqlState $sqlState The SQLSTATE error code (typically 23502)
     */
    public static function notNullViolation(
        string $table,
        string $column,
        SqlState $sqlState
    ): self {
        $message = "NOT NULL constraint violation on '{$table}.{$column}': value cannot be null";

        $context = ExceptionContext::forOperation('constraint.not_null', 'Database')
            ->withData([
                'table' => $table,
                'column' => $column,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'not_null',
            ]);

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

    /**
     * Create exception for CHECK constraint violation
     *
     * @param string $table Table name
     * @param string $constraintName Check constraint name
     * @param SqlState $sqlState The SQLSTATE error code (typically 23514)
     * @param string|null $checkCondition Optional check condition description
     */
    public static function checkViolation(
        string $table,
        string $constraintName,
        SqlState $sqlState,
        ?string $checkCondition = null
    ): self {
        $message = "CHECK constraint '{$constraintName}' violated on table '{$table}'";

        if ($checkCondition !== null) {
            $message .= ": {$checkCondition}";
        }

        $context = ExceptionContext::forOperation('constraint.check', 'Database')
            ->withData([
                'table' => $table,
                'constraint_name' => $constraintName,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'check',
            ]);

        if ($checkCondition !== null) {
            $context = $context->withDebug(['check_condition' => $checkCondition]);
        }

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

    /**
     * Create exception for primary key violation
     *
     * @param string $table Table name
     * @param string $primaryKey Primary key column(s)
     * @param mixed $value The duplicate value
     * @param SqlState $sqlState The SQLSTATE error code (typically 23505)
     */
    public static function primaryKeyViolation(
        string $table,
        string $primaryKey,
        mixed $value,
        SqlState $sqlState
    ): self {
        $message = "Primary key constraint violation on '{$table}.{$primaryKey}': value already exists";

        $context = ExceptionContext::forOperation('constraint.primary_key', 'Database')
            ->withData([
                'table' => $table,
                'primary_key' => $primaryKey,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'primary_key',
            ])
            ->withDebug([
                'duplicate_value' => self::sanitizeValue($value),
            ]);

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

    /**
     * Create exception for composite unique constraint violation
     *
     * @param string $table Table name
     * @param array<string> $columns Array of column names
     * @param array<string, mixed> $values Array of column => value pairs
     * @param SqlState $sqlState The SQLSTATE error code (typically 23505)
     */
    public static function compositeUniqueViolation(
        string $table,
        array $columns,
        array $values,
        SqlState $sqlState
    ): self {
        $columnList = implode(', ', $columns);
        $message = "Composite unique constraint violation on '{$table}' (" . $columnList . "): combination already exists";

        $context = ExceptionContext::forOperation('constraint.composite_unique', 'Database')
            ->withData([
                'table' => $table,
                'columns' => $columns,
                'sqlstate' => $sqlState->code,
                'error_category' => 'Integrity Constraint Violation',
                'constraint_type' => 'composite_unique',
            ])
            ->withDebug([
                'duplicate_values' => self::sanitizeArray($values),
            ]);

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

    /**
     * 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;
    }

    /**
     * Sanitize array for logging
     *
     * @param array<string, mixed> $values Array to sanitize
     * @return array<string, string> Sanitized array
     */
    private static function sanitizeArray(array $values): array
    {
        $sanitized = [];
        foreach ($values as $key => $value) {
            $sanitized[$key] = self::sanitizeValue($value);
        }
        return $sanitized;
    }
}
