<?php

namespace Formatrica\Submission\Validation;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
/**
 * Validates file uploads for security and compliance.
 *
 * Single-file (`file`) and multi-file (Pro `multi_file`) both route their
 * per-file checks through validate_one() so the security rules stay identical.
 */
final class File_Validator
{
    private const ALLOWED_FILE_TYPES = [
        'image/jpeg',
        'image/png',
        'image/gif',
        'image/webp',
        'application/pdf',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'text/plain',
        'text/csv',
    ];

    /**
     * Validate a single-file field.
     *
     * @param array<string, mixed> $field
     * @param array<string, mixed> $files
     * @return string|null Error message or null if valid
     */
    public function validate(array $field, array $files): ?string
    {
        $name = $field['name'] ?? '';

        if ($name === '') {
            return null;
        }

        $file = $this->extract_uploaded_file($name, $files);

        if (!$file || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            return null;
        }

        return $this->validate_one($file, $field);
    }

    /**
     * Per-file security + compliance checks (size, MIME allow-list with finfo
     * cross-check, image-dimension cap). Shared by single and multi file.
     *
     * @param array<string, mixed> $file  A single $_FILES-shaped entry.
     * @param array<string, mixed> $field
     * @return string|null
     */
    public function validate_one(array $file, array $field): ?string
    {
        if ((int) ($file['error'] ?? 0) !== UPLOAD_ERR_OK) {
            return __('File upload failed.', 'formatrica');
        }

        // Check file size
        $field_max_size_mb = isset($field['maxFileSize']) ? (int) $field['maxFileSize'] : 5;
        $field_max_size_mb = max(1, min(50, $field_max_size_mb));
        $max_size = $field_max_size_mb * 1024 * 1024;
        $max_size = (int) apply_filters('formatrica_max_upload_size', $max_size, $field);

        if (($file['size'] ?? 0) > $max_size) {
            /* translators: %d: maximum file size in megabytes */
            return sprintf(__('File size exceeds the %dMB limit.', 'formatrica'), $field_max_size_mb);
        }

        $final_allowed_types = $this->resolve_allowed_types($field);

        // Check file type
        $check = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
        $type = $check['type'] ?? '';

        if ($type === '' || !in_array($type, $final_allowed_types, true)) {
            return __('This file type is not allowed.', 'formatrica');
        }

        // Double-check with finfo if available
        if (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            if ($finfo !== false) {
                $detected_mime = finfo_file($finfo, $file['tmp_name']);
                finfo_close($finfo);

                if ($detected_mime === false || !$this->detected_mime_matches($type, $detected_mime)) {
                    return __('File content does not match allowed types.', 'formatrica');
                }
            }
        }

        if (in_array($type, [
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        ], true) && !$this->valid_office_archive($file['tmp_name'], $type)) {
            return __('File content does not match allowed types.', 'formatrica');
        }

        // Validate image dimensions if it's an image
        if (str_starts_with($type, 'image/')) {
            $image_info = @getimagesize($file['tmp_name']);
            if ($image_info === false) {
                return __('Invalid image file.', 'formatrica');
            }

            [$width, $height] = $image_info;
            $max_dimension = (int) apply_filters('formatrica_max_image_dimension', 4096, $field);

            if ($width > $max_dimension || $height > $max_dimension) {
                /* translators: 1: max width in pixels, 2: max height in pixels */
                return sprintf(__('Image dimensions exceed %1$dx%2$d pixels.', 'formatrica'), $max_dimension, $max_dimension);
            }
        }

        return null;
    }

    /** @return list<string> */
    public static function default_allowed_types(): array
    {
        return self::ALLOWED_FILE_TYPES;
    }

    private function detected_mime_matches(string $declared, string $detected): bool
    {
        $aliases = [
            'text/csv' => ['text/csv', 'text/plain', 'application/csv'],
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['application/zip', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['application/zip', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
        ];

        return in_array($detected, $aliases[$declared] ?? [$declared], true);
    }

    private function valid_office_archive(string $path, string $type): bool
    {
        if (!class_exists('ZipArchive')) {
            return false;
        }

        $required = $type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
            ? ['[Content_Types].xml', 'word/document.xml']
            : ['[Content_Types].xml', 'xl/workbook.xml'];

        $zip = new \ZipArchive();
        if ($zip->open($path) !== true) {
            return false;
        }

        foreach ($required as $entry) {
            if ($zip->locateName($entry) === false) {
                $zip->close();
                return false;
            }
        }

        $zip->close();
        return true;
    }

    /**
     * Resolve the effective allow-list for a field: intersect its `accept` list
     * with the global allow-list; fall back to the full list when no accept set.
     *
     * @param array<string, mixed> $field
     * @return list<string>
     */
    public function resolve_allowed_types(array $field): array
    {
        $allowed_types = apply_filters('formatrica_allowed_file_types', self::ALLOWED_FILE_TYPES, $field);

        $field_accept = isset($field['accept']) && is_string($field['accept']) ? $field['accept'] : '';
        $field_allowed_types = [];

        if ($field_accept !== '') {
            $accept_parts = array_map('trim', explode(',', $field_accept));
            foreach ($accept_parts as $mime) {
                if (in_array($mime, $allowed_types, true)) {
                    $field_allowed_types[] = $mime;
                }
            }
        }

        return !empty($field_allowed_types) ? $field_allowed_types : $allowed_types;
    }

    /**
     * Extract a single uploaded file from the $_FILES array.
     *
     * @param string $name
     * @param array<string, mixed> $files
     * @return array<string, mixed>|null
     */
    public function extract_uploaded_file(string $name, array $files): ?array
    {
        if (isset($files[$name]) && isset($files[$name]['tmp_name']) && !is_array($files[$name]['tmp_name'])) {
            return $files[$name];
        }

        if (!isset($files['files'])) {
            return null;
        }

        $group = $files['files'];

        if (
            isset($group['name'][$name]) &&
            isset($group['tmp_name'][$name]) &&
            isset($group['error'][$name]) &&
            isset($group['size'][$name]) &&
            !is_array($group['tmp_name'][$name])
        ) {
            return [
                'name' => $group['name'][$name],
                'type' => $group['type'][$name] ?? '',
                'tmp_name' => $group['tmp_name'][$name],
                'error' => $group['error'][$name],
                'size' => $group['size'][$name],
            ];
        }

        return null;
    }

}
