<?php

declare(strict_types=1);

namespace PostService\Node;

final class FileFingerprint
{
    public const VERSION = 'partial-sha256-v1';
    private const CHUNK_SIZE = 1048576;

    public static function calculate(string $path, int $size): string
    {
        $handle = @fopen($path, 'rb');
        if ($handle === false) {
            throw new ApiException('File could not be opened for hashing', 'file_unreadable', 422);
        }

        try {
            if ($size < self::CHUNK_SIZE * 2) {
                $context = hash_init('sha256');
                $bytesRead = hash_update_stream($context, $handle);
                if ($bytesRead !== $size) {
                    throw new ApiException('File changed while it was being hashed', 'file_changed', 409);
                }
                return hash_final($context);
            }

            $first = self::readExactly($handle, self::CHUNK_SIZE);
            if (fseek($handle, -self::CHUNK_SIZE, SEEK_END) !== 0) {
                throw new ApiException('File tail could not be read for hashing', 'file_unreadable', 422);
            }
            $last = self::readExactly($handle, self::CHUNK_SIZE);

            // This deliberately preserves the historical algorithm byte-for-byte.
            return hash('sha256', (string) $size . $first . $last);
        } finally {
            fclose($handle);
        }
    }

    private static function readExactly($handle, int $length): string
    {
        $data = '';
        while (strlen($data) < $length && !feof($handle)) {
            $chunk = fread($handle, $length - strlen($data));
            if ($chunk === false) {
                throw new ApiException('File could not be read for hashing', 'file_unreadable', 422);
            }
            $data .= $chunk;
        }
        if (strlen($data) !== $length) {
            throw new ApiException('File changed while it was being hashed', 'file_changed', 409);
        }
        return $data;
    }
}
