<?php

namespace GriffinForms\Frontend\Html\Forms\Fields;

final class FileUpload extends \GriffinForms\Frontend\Html\Forms\Fields\Format
{
    protected $pseudo_class;
    protected $hydrated_file_meta = [];

    protected function setPseudoClass()
    {
        $this->pseudo_class = $this->html_ids['formcontrol'] . '-display';
    }

    protected function syncRenderedValue(int $iteration = 0): void
    {
        parent::syncRenderedValue($iteration);
        $this->hydrated_file_meta = $this->buildHydratedFileMeta($this->value);
    }

    protected function formControl()
    {
        $this->setPseudoClass();
        $field_id = (int) ($this->item->getProp('id') ?? 0);
        $meta_value = '';
        if (!empty($this->hydrated_file_meta)) {
            $meta_value = wp_json_encode($this->hydrated_file_meta);
        } elseif (is_array($this->value) && !empty($this->value)) {
            $meta_value = wp_json_encode($this->value);
        } elseif (is_string($this->value) && $this->value !== '') {
            $meta_value = $this->value;
        }
        $html  = $this->uploadedFilesList();
        $html .= '<input type="file"';
        $html .= ' class="' . $this->pseudo_class . ' form-control mb-1"';
        $html .= $this->requiredAttr();
        $html .= $this->readOnlyAttr();
        $html .= $this->disabledAttr();
        $html .= $this->acceptAttr();
        $html .= $this->multipleAttr();
        $html .= ' data-gf-formpage="' . absint($this->data['formpage']) . '">';
        $html .= '<input type="hidden" name="' . esc_attr( 'gf_file_' . $field_id . '[]' ) . '" class="' . esc_attr( $this->html_ids['formcontrol'] ) . '"';
        $html .= ' data-gf-file-meta="' . esc_attr($meta_value !== '' ? $meta_value : '{}') . '"';
        if ($meta_value !== '') {
            $html .= ' value="' . esc_attr($meta_value) . '"';
        }
        $html .= '>';
        return $html;
    }

    protected function uploadedFilesList()
    {
        // Accessible, initially empty list. JS will populate items after uploads.
        // Use aria-live so screen readers announce changes.
        $list_id = 'gf-fileslist-' . absint($this->item->getProp('id') ?? 0);
        $html  = '<ul id="' . esc_attr($list_id) . '" class="list-group small mb-2" aria-live="polite" aria-relevant="additions removals" hidden>';
        $html .= '</ul>';
        return $html;
    }

    protected function multipleAttr()
    {
        $val = $this->field_settings['allow_multiple_select'] ?? 0;
        $enabled = is_bool($val) ? $val : ((string)$val === '1' || (int)$val === 1);
        return $enabled ? ' multiple="multiple"' : '';
    }

    protected function acceptAttr()
    {
        $parts = array();

        // Allowed mimes/extensions coming from UI (array)
        if (!empty($this->field_settings['allowed_mimes']) && is_array($this->field_settings['allowed_mimes'])) {
            foreach ($this->field_settings['allowed_mimes'] as $p) {
                $p = trim((string)$p);
                if ($p === '') { continue; }
                $parts[] = $p; // may be image/*, .svg, application/pdf, jpg, etc.
            }
        }

        // Custom list entered by user; may be array or string (comma/space/semicolon/newline separated)
        if (!empty($this->field_settings['custom_mimes'])) {
            $extra = array();
            if (is_array($this->field_settings['custom_mimes'])) {
                foreach ($this->field_settings['custom_mimes'] as $tok) {
                    $tok = trim((string) $tok);
                    if ($tok !== '') { $extra[] = $tok; }
                }
            } else {
                $custom = (string) $this->field_settings['custom_mimes'];
                // Normalize delimiters and split
                $custom = str_replace(["\r", "\n", "\t", ';'], ',', $custom);
                $extra  = array_filter(array_map('trim', explode(',', $custom)));
            }
            if (!empty($extra)) {
                $parts = array_merge($parts, $extra);
            }
        }

        if (!empty($parts)) {
            $normalized = array();
            foreach ($parts as $tok) {
                $tok = strtolower(trim($tok));
                if ($tok === '') { continue; }
                if (strpos($tok, '/') !== false) {
                    // MIME type or group (e.g., image/*)
                    $normalized[] = $tok;
                } else {
                    // extension-like token; ensure leading dot
                    if ($tok[0] !== '.') {
                        $tok = '.' . $tok;
                    }
                    $normalized[] = $tok;
                }
            }
            $normalized = array_unique($normalized);
            $accept = implode(',', $normalized);
            return ' accept="' . esc_attr($accept) . '"';
        }

        return '';
    }
    
    protected function fileIconJs()
    {
        // Returns JS for file icon helpers (iconBaseUrl, gfBuildIconUrl, gfGetExt, gfGetFileIcon)
        $iconBaseUrl = esc_url($this->config->getRootUrl()) . 'frontend/images/fileicons';
        $choices = \GriffinForms\Admin\Choices\Field::getInstance();
        $docx = wp_json_encode($choices->getChoices('document_mimes', 0));
        $xlsx = wp_json_encode($choices->getChoices('spreadsheet_mimes', 0));
        $ppt = wp_json_encode($choices->getChoices('presentation_mimes', 0));
        $archive = wp_json_encode($choices->getChoices('archive_mimes', 0));
        $text = wp_json_encode($choices->getChoices('text_mimes', 0));
        $code = wp_json_encode($choices->getChoices('code_mimes', 0));

        return '
        // --- File icon helpers ---
        // Placeholder base URL for icons; set this globally later (e.g., window.gfFileIconBaseUrl)
        var iconBaseUrl = (window.gfFileIconBaseUrl || "' . $iconBaseUrl . '");

        // Extension groups provided by PHP (JSON -> real JS arrays)
        var documentExts   = ' . ($docx ?: '[]') . ';
        var spreadsheetExts= ' . ($xlsx ?: '[]') . ';
        var presentationExts=' . ($ppt ?: '[]') . ';
        var archiveExts    = ' . ($archive ?: '[]') . ';
        var textExts       = ' . ($text ?: '[]') . ';
        var codeExts       = ' . ($code ?: '[]') . ';

        function gfBuildIconUrl(relPath){
            if (!relPath) relPath = "file.svg";
            if (iconBaseUrl.slice(-1) !== "/") iconBaseUrl += "/";
            return iconBaseUrl + relPath;
        }
        function gfGetExt(name){
            if (!name) return "";
            var idx = String(name).lastIndexOf(".");
            return idx >= 0 ? String(name).slice(idx + 1).toLowerCase() : "";
        }
        function gfIn(list, ext){
            if (!Array.isArray(list) || !ext) return false;
            return list.indexOf(ext) !== -1;
        }
        function gfGetFileIcon(type, name){
            var ext = gfGetExt(name);
            type = String(type || "").toLowerCase();
            // MIME-first
            if (type.indexOf("image/") === 0) return "image.svg";
            if (type === "application/pdf" || ext === "pdf") return "pdf.svg";
            if (type.indexOf("audio/") === 0) return "audio.svg";
            if (type.indexOf("video/") === 0) return "video.svg";

            // Extension groups (from PHP)
            if (gfIn(documentExts, ext))    return "doc.svg";
            if (gfIn(spreadsheetExts, ext)) return "xls.svg";
            if (gfIn(presentationExts, ext))return "ppt.svg";
            if (gfIn(archiveExts, ext))     return "archive.svg";
            if (gfIn(textExts, ext))        return "txt.svg";
            if (gfIn(codeExts, ext))        return "code.svg";

            return "file.svg"; // fallback
        }
        ';
    }

    protected function subItemJs()
    {
        $nonce = wp_create_nonce('file_upload_temp');
        $nonce_delete = wp_create_nonce('file_upload_delete');
        $collector = $this->html_ids['formcontrol'];
        $field_id = $this->item->getProp('id') ?? 0;
        $options = $this->item->getProp('field_settings');
        $options_json = wp_json_encode( is_array($options) ? $options : (array) $options, JSON_UNESCAPED_SLASHES );
        $max_upload_size = wp_max_upload_size();

        // Output file icon JS helpers first
        echo $this->fileIconJs();

        echo '
        const ajaxurl = "' . esc_url(admin_url('admin-ajax.php')) . '";
        var pseudoSel = ".' . esc_js($this->pseudo_class) . '";
        var collectorClass = ".' . esc_js($collector) . '";
        var fieldId = ' . (int)$field_id . ';
        var fieldOptions = ' . ($options_json !== null ? $options_json : '[]') . ';

        var deleteNonce = "' . esc_js($nonce_delete) . '";

        // Maximum upload size in bytes (default 10MB if not specified)
        var maxUploadSize = ' . absint($max_upload_size) . ';

        // Check if any file exceeds maxUploadSize, or if total combined exceeds, return true if all good, false otherwise
        function gfCheckFileSizes(files) {
            var totalSize = 0;
            for (var i = 0; i < files.length; i++) {
                if (files[i].size > maxUploadSize) {
                    return false;
                }
                totalSize += files[i].size;
            }
            if (totalSize > maxUploadSize) {
                return false;
            }
            return true;
        }

        // Return the CSS selector for this field\'s uploaded-files <ul>
        function gfListSelector(fid){ return "#gf-fileslist-" + String(fid); }
        // Convert byte size to a human-readable string (e.g., 15360 -> "15 KB")
        function gfFormatBytes(bytes){
            var b = Number(bytes || 0);
            if (!isFinite(b) || b <= 0) return "";
            var u = ["B","KB","MB","GB","TB"];
            var i = 0; while (b >= 1024 && i < u.length-1){ b /= 1024; i++; }
            return b.toFixed(b < 10 && i > 0 ? 1 : 0) + " " + u[i];
        }
        // Get the jQuery object for the uploaded-files list or null if missing
        function gfEnsureList(fid){
            var $list = $(gfListSelector(fid));
            if (!$list.length){ return null; }
            return $list;
        }
        function gfToggleListVisibility(fid){
            var $list = gfEnsureList(fid); if (!$list) return;
            if ($list.children("li").length){
                $list.removeAttr("hidden");
            } else {
                $list.attr("hidden", "hidden");
            }
        }
        // Build one <li> for an uploaded file (name + meta + remove button)
        function gfCreateListItem(fileObj){
            // fileObj: {token, name, size, type, url}
            var li = $("<li/>")
                .addClass("list-group-item d-flex justify-content-between align-items-center small")
                .attr("data-gf-token", String(fileObj.token || ""));

            // Left icon column
            var iconWrap = $("<div/>").addClass("me-2 d-flex align-items-center").css({ width: "36px" });
            var iconImg  = $("<img/>").addClass("gf-file-icon").attr({ alt: "" }).css({ width: "24px", height: "24px" });
            var displayMime = fileObj.mime || fileObj.type;
            var iconRel  = gfGetFileIcon(displayMime, fileObj.name);
            iconImg.attr("src", gfBuildIconUrl(iconRel));
            iconWrap.append(iconImg);

            // Name + meta
            var left = $("<div/>").addClass("me-2 text-truncate flex-fill");
            var nameEl = $("<div/>").addClass("fw-medium me-2 text-truncate").text(fileObj.name || "");
            var meta = [];
            if (displayMime) meta.push(displayMime);
            if (fileObj.size) meta.push(gfFormatBytes(fileObj.size));
            var metaEl = $("<div/>").addClass("text-muted").text(meta.join(" • "));
            left.append(nameEl).append(metaEl);

            // Remove button
            var btn = $("<button/>", { type: "button", "aria-label": "Remove file" })
                .addClass("btn text-danger gf-file-remove")
                .text("×");

            li.append(iconWrap).append(left).append(btn);
            return li;
        }
        // Read all file tokens from the current list items
        function gfGetTokensFromList(fid){
            var $list = gfEnsureList(fid); if (!$list) return [];
            var tokens = [];
            $list.children("li").each(function(){
                var t = $(this).attr("data-gf-token");
                if (t) tokens.push(String(t));
            });
            return tokens;
        }
        function gfReadMetaStore(){
            var el = $(collectorClass).get(0);
            if (!el) { return {}; }
            var raw = el.getAttribute("data-gf-file-meta");
            if (!raw) { return {}; }
            try {
                var parsed = JSON.parse(raw);
                if (Array.isArray(parsed)) {
                    var arrayStore = {};
                    parsed.forEach(function(file){
                        if (!file || !file.token) { return; }
                        arrayStore[String(file.token)] = {
                            name: file.name || "",
                            mime: file.mime || file.type || "",
                            size: typeof file.size === "number" ? file.size : null,
                            type: file.type || "",
                            url: file.url || ""
                        };
                    });
                    return arrayStore;
                }
                if (parsed && typeof parsed === "object") {
                    return parsed;
                }
            } catch (error) {
                // reset below when invalid JSON is encountered
            }
            return {};
        }

        function gfWriteMetaStore(store){
            var el = $(collectorClass);
            if (!el.length) { return; }
            var json;
            try {
                json = JSON.stringify(store || {});
            } catch (error) {
                json = "{}";
            }
            el.attr("data-gf-file-meta", json);
        }

        function gfAddFilesMeta(files){
            if (!Array.isArray(files) || !files.length) { return; }
            var store = gfReadMetaStore();
            files.forEach(function(file){
                if (!file || !file.token) { return; }
                store[String(file.token)] = {
                    name: file.name || "",
                    mime: file.mime || file.type || "",
                    size: typeof file.size === "number" ? file.size : null
                };
            });
            gfWriteMetaStore(store);
        }

        function gfRemoveFileMeta(token){
            if (!token) { return; }
            var store = gfReadMetaStore();
            if (Object.prototype.hasOwnProperty.call(store, token)) {
                delete store[token];
                gfWriteMetaStore(store);
            }
        }
        // Sync the hidden collector input with tokens in the list
        function gfUpdateCollector(fid){
            var tokens = gfGetTokensFromList(fid);
            $(collectorClass).val(tokens.join(","));
            $(collectorClass).trigger("change");
            return tokens;
        }
        // Append server-returned files to the list and refresh the collector
        function gfAppendFilesToList(fid, files){
            var $list = gfEnsureList(fid); if (!$list) return;
            (files || []).forEach(function(f){
                if (f && f.token){ $list.append(gfCreateListItem(f)); }
            });
            gfAddFilesMeta(files);
            gfUpdateCollector(fid);
            gfToggleListVisibility(fid);
        }
        function gfHydrateExistingFiles(fid){
            var store = gfReadMetaStore();
            var files = [];
            Object.keys(store || {}).forEach(function(token){
                var meta = store[token] || {};
                files.push({
                    token: token,
                    name: meta.name || "",
                    mime: meta.mime || meta.type || "",
                    type: meta.type || "",
                    size: typeof meta.size === "number" ? meta.size : null,
                    url: meta.url || ""
                });
            });
            if (files.length) {
                gfAppendFilesToList(fid, files);
            }
        }
        // Reset the <input type="file"> so selecting the same file re-triggers change
        function gfClearFileInput(){
            var $ctl = $(pseudoSel);
            var $new = $ctl.clone();
            $new.val("");
            $ctl.replaceWith($new);
        }
        // Call backend to delete a temporary file by token; disables the button while pending
        function gfDeleteTempFile(fid, token, $btn){
            if (!token) { return; }
            var fd = new FormData();
            fd.append("action", "fileUploadDelete");
            fd.append("_ajax_nonce", deleteNonce);
            fd.append("token", token);
            fd.append("field_id", fid);
            if (window.griffinformsDraftId) { fd.append("draft_id", window.griffinformsDraftId); }
            if (window.griffinformsFormId)  { fd.append("form_id",  window.griffinformsFormId); }

            $btn.prop("disabled", true).addClass("disabled");
            return $.ajax({
                url: ajaxurl,
                method: "POST",
                data: fd,
                processData: false,
                contentType: false,
                dataType: "json"
            }).always(function(){
                $btn.prop("disabled", false).removeClass("disabled");
            });
        }
        // Delegated handler: when user clicks ×, request delete, then remove <li> and update collector
        $(document)
          .off("click.gfUploadRemove", gfListSelector(fieldId) + " .gf-file-remove")
          .on("click.gfUploadRemove", gfListSelector(fieldId) + " .gf-file-remove", function(){
              var $btn = $(this);
              var $li  = $btn.closest("li");
              var token = $li.attr("data-gf-token") || "";
              if (!token) { return; }
              gfDeleteTempFile(fieldId, token, $btn)
                .done(function(response){
                    if (response && response.success) {
                        $li.remove();
                        gfRemoveFileMeta(token);
                        gfUpdateCollector(fieldId);
                        gfToggleListVisibility(fieldId);
                    } else {
                        var msg = (response && response.data && response.data.message) ? response.data.message : "Could not remove file.";
                        alert(msg);
                    }
                })
                .fail(function(jq, status, err){
                    console.log("Delete fail raw:", jq && jq.responseText ? jq.responseText : "");
                    alert(err || status || "Delete request failed");
                });
          });

        // On file selection: upload files via AJAX, then render them in the list (or alert on error)
        $(document).off("change.gfUpload", pseudoSel).on("change.gfUpload", pseudoSel, function () {
            var files = this.files || [];
            if (!files.length) { return; }

            // Client-side preflight size check to prevent 413 errors
            if (!gfCheckFileSizes(files)) {
                alert(("' . esc_js( __( 'One or more selected files are too large. The maximum allowed size is %s.', 'griffinforms-form-builder' ) ) . '").replace("%s", gfFormatBytes(maxUploadSize)));
                gfClearFileInput();
                return;
            }

            var fd = new FormData();
            addWait($(pseudoSel));
            fd.append("action", "fileUploadTemp");
            fd.append("_ajax_nonce", "' . esc_js($nonce) . '");
            if (window.griffinformsDraftId) { fd.append("draft_id", window.griffinformsDraftId); }
            fd.append("field_id", fieldId);
            fd.append("existing_tokens", $(collectorClass).val() || "");
            fd.append("field_settings", JSON.stringify(fieldOptions));
            if (window.griffinformsFormId) { fd.append("form_id", window.griffinformsFormId); }

            for (var i = 0; i < files.length; i++) {
                fd.append("files[]", files[i], files[i].name);
            }

            $.ajax({
                url: ajaxurl,
                method: "POST",
                data: fd,
                processData: false,
                contentType: false,
                dataType: "json"
            }).done(function(response){
                removeWait($(pseudoSel));

                var hasData   = response && response.data;
                var msg       = hasData && response.data.message ? response.data.message : "";
                var filesArr  = hasData && Array.isArray(response.data.files) ? response.data.files : [];
                var errorsArr = hasData && Array.isArray(response.data.errors) ? response.data.errors : [];

                if (response && response.success) {
                    // Render any successfully uploaded files
                    if (filesArr.length) {
                        gfAppendFilesToList(fieldId, filesArr);
                        gfClearFileInput();
                    }
                    // If backend reported partial failures, surface the message
                    if (errorsArr.length) {
                        alert(msg || "Some files could not be uploaded.");
                        gfClearFileInput();
                    }
                    // For full success (no errors), stay silent to match existing UX
                    return;
                }

                // Hard failure path
                alert(msg || "File upload failed.");
                gfClearFileInput();
            }).fail(function(jq, status, err){
                removeWait($(pseudoSel));
                console.log("Upload fail raw:", jq && jq.responseText ? jq.responseText : "");
                alert(err || status || "Upload error");
                gfClearFileInput();
            });
        });

        gfHydrateExistingFiles(fieldId);
        gfToggleListVisibility(fieldId);';
    }

    private function buildHydratedFileMeta($value): array
    {
        $tokens = $this->extractFileTokens($value);
        if (empty($tokens)) {
            return [];
        }

        $sql = \GriffinForms\Admin\Sql\Submission::getInstance();
        $store = [];

        foreach ($tokens as $token) {
            $row = $sql->fetchFileRowByToken($token);
            $name = $sql->getFileNameFromToken($token);
            $store[$token] = [
                'name' => $name,
                'mime' => is_array($row) ? (string) ($row['mime'] ?? '') : '',
                'size' => is_array($row) && isset($row['size_bytes']) ? (int) $row['size_bytes'] : null,
                'type' => is_array($row) ? (string) ($row['mime'] ?? '') : '',
                'url'  => is_array($row) ? (string) ($row['url'] ?? '') : '',
            ];
        }

        return $store;
    }

    private function extractFileTokens($value): array
    {
        if (is_string($value)) {
            $decoded = json_decode($value, true);
            if (json_last_error() === JSON_ERROR_NONE) {
                $value = $decoded;
            }
        }

        $tokens = [];

        if (is_string($value)) {
            $tokens = preg_split('/\s*,\s*/', trim($value)) ?: [];
        } elseif (is_array($value)) {
            foreach ($value as $key => $item) {
                if (is_string($key) && preg_match('/^[a-f0-9]{40}$/i', $key)) {
                    $tokens[] = $key;
                    continue;
                }

                if (is_string($item) && preg_match('/^[a-f0-9]{40}$/i', $item)) {
                    $tokens[] = $item;
                    continue;
                }

                if (is_array($item) && !empty($item['token']) && is_string($item['token'])) {
                    $tokens[] = $item['token'];
                }
            }
        }

        $tokens = array_filter(array_map('trim', $tokens), static function ($token) {
            return $token !== '' && preg_match('/^[a-f0-9]{40}$/i', $token);
        });

        return array_values(array_unique($tokens));
    }
}
