<?php

declare(strict_types=1);

namespace App\Helpers;

use RuntimeException;

/**
 * UploadHelper
 *
 * Handles every file upload in the app (avatars, lead/customer
 * attachments, CSV imports). Deliberately paranoid:
 *   - Never trusts the client-supplied MIME type or extension alone;
 *     sniffs the real content with finfo.
 *   - Renames every file to a random name on disk (original name is
 *     kept only in the `attachments` table for display).
 *   - Stores everything under /uploads, which sits OUTSIDE the public
 *     web root's PHP-executable tree (see docs/ARCHITECTURE.md) and is
 *     served only through a controller that checks permissions first —
 *     never linked to directly.
 */
final class UploadHelper
{
    private array $errors = [];

    public function __construct(private readonly array $config = [])
    {
    }

    public static function make(): self
    {
        return new self(config('security.uploads'));
    }

    public function errors(): array
    {
        return $this->errors;
    }

    /**
     * @param array $file One entry from $_FILES, e.g. $request->file('attachment')
     * @return array{disk_path:string,original_name:string,mime_type:string,size_bytes:int,checksum_sha256:string}|null
     */
    public function store(array $file, string $subdirectory = 'attachments'): ?array
    {
        $this->errors = [];

        if (!$this->validateBasics($file)) {
            return null;
        }

        $realMime = $this->detectRealMimeType($file['tmp_name']);
        $allowed = $this->config['allowed_mime_types'] ?? [];

        if (!in_array($realMime, $allowed, true)) {
            $this->errors[] = 'That file type is not allowed.';

            return null;
        }

        $maxBytes = $this->config['max_size_bytes'] ?? (15 * 1024 * 1024);
        if ($file['size'] > $maxBytes) {
            $this->errors[] = 'File is larger than the ' . number_format($maxBytes / 1024 / 1024, 0) . 'MB limit.';

            return null;
        }

        $extension = $this->safeExtension($file['name'], $realMime);
        $randomName = bin2hex(random_bytes(20)) . ($extension ? ".{$extension}" : '');

        $subdirectory = trim($subdirectory, '/');
        $targetDir = $this->uploadsRoot() . '/' . $subdirectory . '/' . date('Y/m');

        if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
            $this->errors[] = 'Could not create the upload directory.';

            return null;
        }

        $targetPath = $targetDir . '/' . $randomName;

        if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
            $this->errors[] = 'The file could not be saved.';

            return null;
        }

        chmod($targetPath, 0644);

        return [
            'disk_path' => $subdirectory . '/' . date('Y/m') . '/' . $randomName,
            'original_name' => $this->sanitizeOriginalName($file['name']),
            'mime_type' => $realMime,
            'size_bytes' => (int) $file['size'],
            'checksum_sha256' => hash_file('sha256', $targetPath),
        ];
    }

    private function validateBasics(array $file): bool
    {
        if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = $this->uploadErrorMessage($file['error'] ?? UPLOAD_ERR_NO_FILE);

            return false;
        }

        if (!is_uploaded_file($file['tmp_name'])) {
            // Defends against path-injection attempts that bypass the
            // normal $_FILES upload mechanism.
            $this->errors[] = 'Invalid upload.';

            return false;
        }

        return true;
    }

    private function uploadErrorMessage(int $code): string
    {
        return match ($code) {
            UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The file is too large.',
            UPLOAD_ERR_PARTIAL => 'The file was only partially uploaded.',
            UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
            default => 'The file upload failed.',
        };
    }

    private function detectRealMimeType(string $tmpPath): string
    {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $tmpPath) ?: 'application/octet-stream';
        finfo_close($finfo);

        return $mime;
    }

    private function safeExtension(string $originalName, string $realMime): string
    {
        $map = [
            'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif',
            'application/pdf' => 'pdf',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
            'text/csv' => 'csv', 'text/plain' => 'txt',
        ];

        // Prefer the extension derived from the SNIFFED mime type, not
        // the client-supplied filename — this is what stops a
        // "resume.pdf.php" upload from ever landing with a .php extension.
        return $map[$realMime] ?? strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
    }

    private function sanitizeOriginalName(string $name): string
    {
        $name = basename($name);

        return substr(preg_replace('/[^A-Za-z0-9 ._-]/', '', $name) ?? $name, 0, 255);
    }

    private function uploadsRoot(): string
    {
        $root = dirname(__DIR__, 2) . '/uploads';

        if (!is_dir($root)) {
            throw new RuntimeException('Uploads root directory does not exist.');
        }

        return $root;
    }
}
