<?php

declare(strict_types=1);

namespace App\Framework\Core\ValueObjects;

use InvalidArgumentException;

final readonly class Hash
{
    public function __construct(
        private string $value,
        private HashAlgorithm $algorithm
    ) {
        $this->validateHash();
    }

    // Factory Methods
    public static function create(string $data, HashAlgorithm $algorithm = HashAlgorithm::SHA256): self
    {
        if (! $algorithm->isAvailable()) {
            throw new InvalidArgumentException("Hash algorithm {$algorithm->value} is not available");
        }

        $hash = hash($algorithm->value, $data);

        return new self($hash, $algorithm);
    }

    public static function fromString(string $hash, HashAlgorithm $algorithm = HashAlgorithm::SHA256): self
    {
        return new self($hash, $algorithm);
    }

    public static function fromFile(string $filePath, HashAlgorithm $algorithm = HashAlgorithm::SHA256): self
    {
        if (! file_exists($filePath)) {
            throw new InvalidArgumentException("File does not exist: $filePath");
        }

        if (! $algorithm->isAvailable()) {
            throw new InvalidArgumentException("Hash algorithm {$algorithm->value} is not available");
        }

        $hash = hash_file($algorithm->value, $filePath);
        if ($hash === false) {
            throw new InvalidArgumentException("Failed to hash file: $filePath");
        }

        return new self($hash, $algorithm);
    }

    // Convenience methods for common algorithms
    public static function md5(string $data): self
    {
        return self::create($data, HashAlgorithm::MD5);
    }

    public static function sha1(string $data): self
    {
        return self::create($data, HashAlgorithm::SHA1);
    }

    public static function sha256(string $data): self
    {
        return self::create($data, HashAlgorithm::SHA256);
    }

    public static function sha512(string $data): self
    {
        return self::create($data, HashAlgorithm::SHA512);
    }

    // Parse from prefixed format like "sha256:abc123..."
    public static function parse(string $value): self
    {
        if (str_contains($value, ':')) {
            [$algoString, $hash] = explode(':', $value, 2);

            $algorithm = HashAlgorithm::tryFrom($algoString);
            if ($algorithm === null) {
                throw new InvalidArgumentException("Unknown hash algorithm: $algoString");
            }

            return new self($hash, $algorithm);
        }

        // Try to detect algorithm by length
        $algorithm = match (strlen($value)) {
            32 => HashAlgorithm::MD5,
            40 => HashAlgorithm::SHA1,
            64 => HashAlgorithm::SHA256,
            128 => HashAlgorithm::SHA512,
            default => throw new InvalidArgumentException("Cannot detect hash algorithm from length"),
        };

        return new self($value, $algorithm);
    }

    // Getters
    public function getValue(): string
    {
        return $this->value;
    }

    public function getAlgorithm(): HashAlgorithm
    {
        return $this->algorithm;
    }

    // Verification
    public function verify(string $data): bool
    {
        $expectedHash = hash($this->algorithm->value, $data);

        return hash_equals($this->value, $expectedHash);
    }

    public function verifyFile(string $filePath): bool
    {
        if (! file_exists($filePath)) {
            return false;
        }

        $fileHash = hash_file($this->algorithm->value, $filePath);
        if ($fileHash === false) {
            return false;
        }

        return hash_equals($this->value, $fileHash);
    }

    // Comparison
    public function equals(Hash $other): bool
    {
        return $this->algorithm === $other->algorithm
            && hash_equals($this->value, $other->value);
    }

    // Formatting
    public function toString(): string
    {
        return $this->value;
    }

    public function toPrefixedString(): string
    {
        return $this->algorithm->value . ':' . $this->value;
    }

    public function toShort(int $length = 8): string
    {
        return substr($this->value, 0, $length);
    }

    // Security checks
    public function isSecure(): bool
    {
        return $this->algorithm->isSecure();
    }

    // String representation
    public function __toString(): string
    {
        return $this->value;
    }

    // Validation
    private function validateHash(): void
    {
        $expectedLength = $this->algorithm->getLength();
        $actualLength = strlen($this->value);

        if ($actualLength !== $expectedLength) {
            throw new InvalidArgumentException(
                "Invalid hash length for {$this->algorithm->value}: expected $expectedLength, got $actualLength"
            );
        }

        if (! ctype_xdigit($this->value)) {
            throw new InvalidArgumentException('Hash must contain only hexadecimal characters');
        }
    }
}
