<?php
namespace BLR\Backline\Core;

if (!defined('ABSPATH')) {
    exit;
}

final class CrudHelpers {
    private const OUTREACH_MAX_ATTACHMENTS = 5;
    private const OUTREACH_MAX_FILE_SIZE = 10485760; // 10 MB per file.
    private const AUDIO_MAX_FILE_SIZE = 262144000; // 250 MB per file.
    private const VISUAL_MAX_FILE_SIZE = 104857600; // 100 MB per file.
    private static $last_upload_errors = [];

    public static function badge(string $status): string {
        $map = [
            'publish' => [__('Published', 'blr-backline'), 'success'],
            'draft' => [__('Draft', 'blr-backline'), 'secondary'],
            'pending' => [__('Pending', 'blr-backline'), 'warning'],
            'private' => [__('Private', 'blr-backline'), 'dark'],
        ];
        $pair = $map[$status] ?? [ucfirst($status ?: '—'), 'secondary'];
        return '<span class="badge bg-' . esc_attr($pair[1]) . '">' . esc_html($pair[0]) . '</span>';
    }

    public static function image_mimes(): array {
        return [
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
        ];
    }

    public static function audio_mimes(): array {
        return [
            'mp3|m4a|m4b' => 'audio/mpeg',
            'wav' => 'audio/wav',
            'ogg|oga' => 'audio/ogg',
            'flac' => 'audio/flac',
            'aac' => 'audio/aac',
            'opus' => 'audio/opus',
        ];
    }

    public static function visual_mimes(): array {
        return [
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'mp4|m4v' => 'video/mp4',
            'mov|qt' => 'video/quicktime',
            'webm' => 'video/webm',
            'ogv' => 'video/ogg',
        ];
    }

    public static function outreach_attachment_mimes(): array {
        return [
            'pdf' => 'application/pdf',
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'mp3|m4a|m4b' => 'audio/mpeg',
            'wav' => 'audio/wav',
            'ogg|oga' => 'audio/ogg',
        ];
    }

    public static function get_last_upload_errors(): array {
        return self::$last_upload_errors;
    }

    public static function clear_last_upload_errors(): void {
        self::$last_upload_errors = [];
    }

    private static function add_upload_error(string $message): void {
        self::$last_upload_errors[] = $message;
    }

    private static function upload_error_message(int $code, string $name): string {
        switch ($code) {
            case UPLOAD_ERR_OK:
                return '';
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                return sprintf(
                    /* translators: %s: file name. */
                    __('%s exceeds the server upload limit.', 'blr-backline'),
                    $name !== '' ? $name : __('The selected file', 'blr-backline')
                );
            case UPLOAD_ERR_PARTIAL:
                return sprintf(
                    /* translators: %s: file name. */
                    __('%s was only partially uploaded.', 'blr-backline'),
                    $name !== '' ? $name : __('The selected file', 'blr-backline')
                );
            case UPLOAD_ERR_NO_FILE:
                return sprintf(
                    /* translators: %s: file name. */
                    __('%s was not received by the server.', 'blr-backline'),
                    $name !== '' ? $name : __('The selected file', 'blr-backline')
                );
            case UPLOAD_ERR_NO_TMP_DIR:
                return __('The server is missing a temporary upload directory.', 'blr-backline');
            case UPLOAD_ERR_CANT_WRITE:
                return sprintf(
                    /* translators: %s: file name. */
                    __('%s could not be written to disk.', 'blr-backline'),
                    $name !== '' ? $name : __('The selected file', 'blr-backline')
                );
            case UPLOAD_ERR_EXTENSION:
                return sprintf(
                    /* translators: %s: file name. */
                    __('%s was blocked by a server extension.', 'blr-backline'),
                    $name !== '' ? $name : __('The selected file', 'blr-backline')
                );
            default:
                return sprintf(
                    /* translators: %s: file name. */
                    __('%s could not be uploaded due to a server error.', 'blr-backline'),
                    $name !== '' ? $name : __('The selected file', 'blr-backline')
                );
        }
    }

    private static function delete_uploaded_file(string $path): void {
        $path = (string) $path;
        if ($path === '' || !file_exists($path)) {
            return;
        }

        wp_delete_file($path);
    }

    /**
     * Prepare a PHP upload array before validation or storage.
     *
     * The browser-provided file name and MIME type are user-controlled. Keep the
     * temporary path intact for WordPress upload handling, sanitize the name, and
     * derive the MIME type from WordPress' filetype checks instead of trusting
     * the request payload.
     *
     * @param array<string,mixed> $file
     * @return array{name:string,type:string,tmp_name:string,error:int,size:int}
     */
    private static function prepare_upload_file(array $file): array {
        $name = sanitize_file_name((string) ($file['name'] ?? ''));
        $tmp_name = (string) ($file['tmp_name'] ?? '');
        $type = '';

        if ($name !== '' && $tmp_name !== '' && file_exists($tmp_name)) {
            $filetype = wp_check_filetype_and_ext($tmp_name, $name);
            $type = is_array($filetype) ? (string) ($filetype['type'] ?? '') : '';
        }

        return [
            'name' => $name,
            'type' => $type,
            'tmp_name' => $tmp_name,
            'error' => isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_OK,
            'size' => isset($file['size']) ? (int) $file['size'] : 0,
        ];
    }

    public static function delete_attachment(int $attachment_id): bool {
        $attachment_id = absint($attachment_id);
        if ($attachment_id <= 0) {
            return false;
        }

        $deleted = wp_delete_attachment($attachment_id, true);
        return !empty($deleted);
    }

    /**
     * @param array{attachment_id?:int}|null $upload
     */
    public static function cleanup_upload_result(?array $upload): void {
        if (!is_array($upload)) {
            return;
        }

        self::delete_attachment(absint($upload['attachment_id'] ?? 0));
    }

    /**
     * @param list<array{attachment_id?:int}> $uploads
     */
    public static function cleanup_upload_results(array $uploads): void {
        foreach ($uploads as $upload) {
            self::cleanup_upload_result($upload);
        }
    }

    public static function count_selected_uploads(string $field): int {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Read-only inspection of the PHP upload array before controller-level processing.
        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw file arrays are required to determine whether uploads were selected.
        if (empty($_FILES[$field]) || !is_array($_FILES[$field])) {
            return 0;
        }

        $names = $_FILES[$field]['name'] ?? null;
        if (is_array($names)) {
            $count = 0;
            foreach ($names as $name) {
                if (sanitize_file_name((string) $name) !== '') {
                    $count++;
                }
            }
            return $count;
        }

        return sanitize_file_name((string) $names) !== '' ? 1 : 0;
        // phpcs:enable WordPress.Security.NonceVerification.Missing
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    }

    private static function validate_upload_array(array $file, array $allowed_mimes, int $max_bytes): bool {
        $name = sanitize_file_name((string) ($file['name'] ?? ''));
        $size = (int) ($file['size'] ?? 0);
        $tmp = (string) ($file['tmp_name'] ?? '');
        $error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_OK;

        if ($name === '' || ($tmp === '' && $error !== UPLOAD_ERR_NO_FILE)) {
            self::add_upload_error(__('A selected upload could not be read.', 'blr-backline'));
            return false;
        }

        if ($error !== UPLOAD_ERR_OK) {
            self::add_upload_error(self::upload_error_message($error, $name));
            return false;
        }

        if ($size <= 0) {
            /* translators: %s: file name. */
            self::add_upload_error(sprintf(__('%s appears to be empty.', 'blr-backline'), $name));
            return false;
        }

        if ($size > $max_bytes) {
            /* translators: 1: file name, 2: formatted file size limit. */
            self::add_upload_error(sprintf(__('%1$s exceeds the %2$s per-file limit.', 'blr-backline'), $name, size_format($max_bytes)));
            return false;
        }

        $check = wp_check_filetype_and_ext($tmp, $name, $allowed_mimes ?: null);
        $ext = (string) ($check['ext'] ?? '');
        $type = (string) ($check['type'] ?? '');
        if ($ext === '' || $type === '') {
            /* translators: %s: file name. */
            self::add_upload_error(sprintf(__('%s is not an allowed attachment type.', 'blr-backline'), $name));
            return false;
        }

        return true;
    }

    public static function has_selected_uploads(string $field): bool {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Read-only inspection of the PHP upload array before controller-level processing.
        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw file arrays are required to determine whether an upload was attempted.
        if (empty($_FILES[$field]) || !is_array($_FILES[$field])) {
            return false;
        }

        $names = $_FILES[$field]['name'] ?? null;
        if (is_array($names)) {
            foreach ($names as $name) {
                if (sanitize_file_name((string) $name) !== '') {
                    return true;
                }
            }
            return false;
        }

        return sanitize_file_name((string) $names) !== '';
        // phpcs:enable WordPress.Security.NonceVerification.Missing
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    }

    /**
     * Validate an upload field without moving files. Returns a list of validation errors.
     *
     * @param array<string,string> $allowed_mimes
     * @return list<string>
     */
    public static function validate_upload_selection(string $field, array $allowed_mimes = [], int $max_bytes = self::OUTREACH_MAX_FILE_SIZE, int $max_files = self::OUTREACH_MAX_ATTACHMENTS): array {
        self::clear_last_upload_errors();

        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Read-only inspection of the PHP upload array before controller-level processing.
        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw file arrays are required for upload validation.
        if (empty($_FILES[$field]) || !is_array($_FILES[$field])) {
            return [];
        }

        $names = $_FILES[$field]['name'] ?? null;
        if (is_array($names)) {
            $selected_indexes = [];
            foreach ($names as $index => $name) {
                if (sanitize_file_name((string) $name) !== '') {
                    $selected_indexes[] = $index;
                }
            }

            if (empty($selected_indexes)) {
                return [];
            }

            if (count($selected_indexes) > $max_files) {
                /* translators: %d: maximum number of files allowed. */
                self::add_upload_error(sprintf(__('Only %d files are allowed for this upload. Extra files were ignored.', 'blr-backline'), $max_files));
                $selected_indexes = array_slice($selected_indexes, 0, $max_files);
            }

            foreach ($selected_indexes as $index) {
                self::validate_upload_array(self::prepare_upload_file([
                    'name' => sanitize_file_name((string) ($_FILES[$field]['name'][$index] ?? '')),
                    'tmp_name' => $_FILES[$field]['tmp_name'][$index] ?? '',
                    'error' => $_FILES[$field]['error'][$index] ?? 0,
                    'size' => $_FILES[$field]['size'][$index] ?? 0,
                ]), $allowed_mimes, $max_bytes);
            }

            return self::get_last_upload_errors();
        }

        if (sanitize_file_name((string) $names) === '') {
            return [];
        }

        self::validate_upload_array(self::prepare_upload_file($_FILES[$field]), $allowed_mimes, $max_bytes);
        return self::get_last_upload_errors();
        // phpcs:enable WordPress.Security.NonceVerification.Missing
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    }

    /**
     * Handle a single file upload and optionally create an attachment.
     *
     * @param array<string,string> $allowed_mimes
     * @return array{attachment_id:int,url:string}|null
     */
    public static function handle_upload(string $field, int $parent_post_id = 0, array $allowed_mimes = [], int $max_bytes = self::OUTREACH_MAX_FILE_SIZE): ?array {
        self::clear_last_upload_errors();

        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonces are verified by the calling controller before file helpers run.
        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw file arrays are required by wp_handle_upload().
        if (empty($_FILES[$field]) || !is_array($_FILES[$field]) || empty($_FILES[$field]['name'])) {
            return null;
        }

        if (!function_exists('wp_handle_upload')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (!function_exists('wp_read_audio_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }
        if (!function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $file = self::prepare_upload_file($_FILES[$field]);
        if (!self::validate_upload_array($file, $allowed_mimes, $max_bytes)) {
            return null;
        }

        $overrides = ['test_form' => false];
        if (!empty($allowed_mimes)) {
            $overrides['mimes'] = $allowed_mimes;
        }

        $moved = wp_handle_upload($file, $overrides);
        // phpcs:enable WordPress.Security.NonceVerification.Missing
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        if (isset($moved['error'])) {
            self::add_upload_error((string) $moved['error']);
            return null;
        }

        $url = (string) ($moved['url'] ?? '');
        $path = (string) ($moved['file'] ?? '');
        $type = (string) ($moved['type'] ?? '');
        if ($url === '' || $path === '') {
            self::add_upload_error(__('The uploaded file could not be stored.', 'blr-backline'));
            return null;
        }

        $attachment = [
            'post_mime_type' => $type,
            'post_title' => sanitize_file_name(basename($path)),
            'post_content' => '',
            'post_status' => 'inherit',
        ];

        $attach_id = wp_insert_attachment($attachment, $path, $parent_post_id);
        if (is_wp_error($attach_id) || !$attach_id) {
            self::add_upload_error(__('The uploaded file could not be attached to the post.', 'blr-backline'));
            self::delete_uploaded_file($path);
            return null;
        }

        $meta = wp_generate_attachment_metadata($attach_id, $path);
        if (is_array($meta)) {
            wp_update_attachment_metadata($attach_id, $meta);
        }

        return ['attachment_id' => (int) $attach_id, 'url' => $url];
    }

    /**
     * Handle multiple uploads from an <input type="file" name="field[]" multiple>.
     *
     * @param array<string,string> $allowed_mimes
     * @return list<array{attachment_id:int,url:string,filename:string,mime:string}>
     */
    public static function handle_multi_upload(string $field, int $parent_post_id = 0, array $allowed_mimes = [], int $max_bytes = self::OUTREACH_MAX_FILE_SIZE, int $max_files = self::OUTREACH_MAX_ATTACHMENTS): array {
        self::clear_last_upload_errors();

        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonces are verified by the calling controller before file helpers run.
        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw file arrays are required by wp_handle_upload().
        if (empty($_FILES[$field]) || !is_array($_FILES[$field])) {
            return [];
        }

        $names = $_FILES[$field]['name'] ?? null;
        if (!is_array($names) || !array_filter($names, 'strlen')) {
            return [];
        }

        if (!function_exists('wp_handle_upload')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (!function_exists('wp_read_audio_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }
        if (!function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $out = [];
        $count = count($names);
        if ($count > $max_files) {
            /* translators: %d: maximum number of files allowed. */
            self::add_upload_error(sprintf(__('Only %d files are allowed for this upload. Extra files were ignored.', 'blr-backline'), $max_files));
            $count = $max_files;
        }

        for ($i = 0; $i < $count; $i++) {
            $name = sanitize_file_name((string) ($_FILES[$field]['name'][$i] ?? ''));
            if ($name === '') {
                continue;
            }

            $file = self::prepare_upload_file([
                'name' => sanitize_file_name((string) ($_FILES[$field]['name'][$i] ?? '')),
                'tmp_name' => $_FILES[$field]['tmp_name'][$i] ?? '',
                'error' => $_FILES[$field]['error'][$i] ?? 0,
                'size' => $_FILES[$field]['size'][$i] ?? 0,
            ]);

            if (!self::validate_upload_array($file, $allowed_mimes, $max_bytes)) {
                continue;
            }

            $overrides = ['test_form' => false];
            if (!empty($allowed_mimes)) {
                $overrides['mimes'] = $allowed_mimes;
            }

            $moved = wp_handle_upload($file, $overrides);
            if (isset($moved['error'])) {
                self::add_upload_error((string) $moved['error']);
                continue;
            }

            $url = (string) ($moved['url'] ?? '');
            $path = (string) ($moved['file'] ?? '');
            $type = (string) ($moved['type'] ?? '');
            if ($url === '' || $path === '') {
                /* translators: %s: file name. */
                self::add_upload_error(sprintf(__('%s could not be stored.', 'blr-backline'), sanitize_file_name($name)));
                continue;
            }

            $attachment = [
                'post_mime_type' => $type,
                'post_title' => sanitize_file_name(basename($path)),
                'post_content' => '',
                'post_status' => 'inherit',
            ];

            $attach_id = wp_insert_attachment($attachment, $path, $parent_post_id);
            $attach_id_int = (!is_wp_error($attach_id) && $attach_id) ? (int) $attach_id : 0;
            if (!$attach_id_int) {
                /* translators: %s: file name. */
                self::add_upload_error(sprintf(__('%s uploaded but could not be attached to the post.', 'blr-backline'), sanitize_file_name($name)));
                self::delete_uploaded_file($path);
                continue;
            }

            $meta = wp_generate_attachment_metadata($attach_id_int, $path);
            if (is_array($meta)) {
                wp_update_attachment_metadata($attach_id_int, $meta);
            }

            $out[] = [
                'attachment_id' => $attach_id_int,
                'url' => $url,
                'filename' => $name,
                'mime' => $type,
            ];
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
        // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

        return $out;
    }


    public static function attachment_duration(int $attachment_id): string {
        $attachment_id = absint($attachment_id);
        if ($attachment_id <= 0) {
            return '';
        }

        $meta = wp_get_attachment_metadata($attachment_id);
        if (!is_array($meta)) {
            return '';
        }

        $formatted = (string) ($meta['length_formatted'] ?? '');
        if ($formatted !== '') {
            return sanitize_text_field($formatted);
        }

        $seconds = isset($meta['length']) ? (int) $meta['length'] : 0;
        if ($seconds <= 0) {
            return '';
        }

        $minutes = (int) floor($seconds / 60);
        $remaining = $seconds % 60;

        return sprintf('%d:%02d', $minutes, $remaining);
    }

    public static function visual_max_file_size(): int {
        return self::VISUAL_MAX_FILE_SIZE;
    }

    public static function audio_max_file_size(): int {
        return self::AUDIO_MAX_FILE_SIZE;
    }

    public static function outreach_max_file_size(): int {
        return self::OUTREACH_MAX_FILE_SIZE;
    }

    public static function upload_max_file_size(): int {
        return self::audio_max_file_size();
    }

    public static function redirect(string $url): void {
        wp_safe_redirect($url);
        exit;
    }
}
