<?php

declare(strict_types=1);

namespace PpPhp\Core\DataOps;

use PpPhp\Core\Events;
use PpPhp\Core\Security;

/**
 * The generated Import wizard, rendered from configuration.
 *
 * import.php is a thin bootstrap that passes a config array; every source,
 * option and format shown here comes from that config plus FormatRegistry, so
 * adding a reader or a Pages-module setting needs no build change and no
 * generated-code change — only this class and the registry.
 *
 * Steps: Source → Mapping → Preview → Import.
 */
final class ImportPage
{
    /**
     * @param array{
     *   table:string, fields:array<string,string>, readers:list<string>,
     *   caption?:string, chrome?:string, options?:array<string,mixed>
     * } $cfg
     */
    public static function handle(array $cfg): void
    {
        $table   = (string) $cfg['table'];
        $fields  = (array) $cfg['fields'];
        $names   = array_keys($fields);
        $readers = array_values((array) ($cfg['readers'] ?? ['csv']));
        $caption = (string) ($cfg['caption'] ?? $table);
        $opt     = (array) ($cfg['options'] ?? []);

        $allowPaste  = ($opt['allowPaste'] ?? true) !== false;
        $allowUpload = ($opt['allowUpload'] ?? true) !== false;
        $modes       = array_values(array_intersect((array) ($opt['modes'] ?? ['insert', 'skip', 'update']), ['insert', 'skip', 'update'])) ?: ['insert'];
        $maxMb       = max(1, (int) ($opt['maxMb'] ?? 8));
        $maxBytes    = $maxMb * 1024 * 1024;
        $matchDefault = (string) ($opt['matchField'] ?? '');
        if ($matchDefault === '' && $names !== []) $matchDefault = (string) $names[0];

        $exts   = FormatRegistry::extensionsFor($readers);
        $accept = implode(',', array_map(static fn(string $e): string => '.' . $e, $exts));
        $extLbl = strtoupper(implode(', ', $exts));

        $dir = dirname(__DIR__, 2) . '/storage/imports';
        if (!is_dir($dir)) @mkdir($dir, 0775, true);

        // ── Sample template (headers only — carries no records) ──
        if (isset($_GET['sample'])) {
            $fmt = FormatRegistry::export((string) ($_GET['sample'] ?: 'csv')) ?? FormatRegistry::export('csv');
            header('Content-Type: ' . $fmt::contentType());
            header('Content-Disposition: attachment; filename="' . $table . '_sample.' . $fmt::extension() . '"');
            $fmt->write([], $fields, ['caption' => $caption, 'table' => $table, 'includeHeaders' => true]);
            exit;
        }

        $step = 'source'; $error = ''; $preview = null; $summary = null;

        if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
            if (!Security::CSRF()->Check($_POST['_token'] ?? '')) {
                $error = 'Invalid session token — please retry.';
            } elseif (($_POST['__do'] ?? '') === 'import') {
                [$step, $error, $summary] = self::commit($table, $names, $dir, $modes);
            } else {
                [$step, $error, $preview] = self::ingest($table, $dir, $readers, $exts, $maxBytes, $allowPaste, $allowUpload, $matchDefault, $modes);
            }
        }

        self::render(compact('table', 'fields', 'names', 'caption', 'step', 'error', 'preview', 'summary')
            + ['cfg' => $cfg, 'readers' => $readers, 'accept' => $accept, 'extLbl' => $extLbl,
               'allowPaste' => $allowPaste, 'allowUpload' => $allowUpload, 'modes' => $modes,
               'maxMb' => $maxMb, 'matchDefault' => $matchDefault, 'opt' => $opt]);
    }

    /** Step 1 → 2: accept pasted text or an upload, store it, parse a preview. */
    private static function ingest(string $table, string $dir, array $readers, array $exts, int $maxBytes, bool $allowPaste, bool $allowUpload, string $matchDefault, array $modes): array
    {
        $paste  = (string) ($_POST['paste'] ?? '');
        $hasFile = !empty($_FILES['file']['name']);
        $token = ''; $path = ''; $error = ''; $readerId = (string) ($_POST['reader'] ?? '');
        $source = 'file';

        if ($allowPaste && trim($paste) !== '') {
            if (strlen($paste) > $maxBytes) {
                $error = 'Pasted data exceeds the ' . (int) ($maxBytes / 1048576) . ' MB limit.';
            } else {
                $source = 'paste';
                if ($readerId === '' || !in_array($readerId, $readers, true)) $readerId = self::sniffPaste($paste, $readers);
                $ext = (FormatRegistry::reader($readerId)::extensions()[0] ?? 'csv');
                $token = 'imp_' . bin2hex(random_bytes(8)) . '.' . $ext;
                $path = $dir . '/' . $token;
                if (@file_put_contents($path, str_replace(["\r\n", "\r"], "\n", $paste)) === false) {
                    $error = 'Could not store the pasted data.'; $token = '';
                }
            }
        } elseif ($allowUpload && $hasFile) {
            $fi = $_FILES['file'];
            $ext = strtolower(pathinfo((string) $fi['name'], PATHINFO_EXTENSION));
            if ($fi['error'] !== UPLOAD_ERR_OK)      $error = 'Upload failed (code ' . $fi['error'] . ').';
            elseif (!in_array($ext, $exts, true))    $error = 'Only these file types may be imported: ' . strtoupper(implode(', ', $exts)) . '.';
            elseif ($fi['size'] > $maxBytes)         $error = 'File exceeds the ' . (int) ($maxBytes / 1048576) . ' MB limit.';
            else {
                $r = FormatRegistry::readerForExtension($ext);
                $readerId = $r ? $r::id() : 'csv';
                $token = 'imp_' . bin2hex(random_bytes(8)) . '.' . $ext;
                $path = $dir . '/' . $token;
                if (!@move_uploaded_file($fi['tmp_name'], $path)) { $error = 'Could not store the uploaded file.'; $token = ''; }
            }
        } else {
            $error = $allowPaste ? 'Paste some rows or choose a file to continue.' : 'Choose a file to continue.';
        }

        if ($error !== '' || $token === '') return ['source', $error, null];

        $opts = ['hasHeader' => !empty($_POST['has_header']), 'delimiter' => (string) ($_POST['delimiter'] ?? 'auto')];
        [$header, $rows] = ImportRunner::read($table, $path, $readerId, $opts);
        if ($rows === []) { @unlink($path); return ['source', 'No data rows were found in that source.', null]; }

        $mode = (string) ($_POST['mode'] ?? 'insert');
        if (!in_array($mode, $modes, true)) $mode = $modes[0];

        return ['map', '', [
            'token' => $token, 'header' => $header, 'rows' => array_slice($rows, 0, 8),
            'total' => count($rows), 'hasHeader' => !empty($_POST['has_header']),
            'delimiter' => $opts['delimiter'], 'reader' => $readerId, 'source' => $source,
            'mode' => $mode, 'matchField' => (string) ($_POST['match_field'] ?? $matchDefault),
            'validate' => !empty($_POST['validate']),
        ]];
    }

    /** Step 3 → 4: re-read with the same options, map, optionally validate, write. */
    private static function commit(string $table, array $names, string $dir, array $modes): array
    {
        $token = basename((string) ($_POST['token'] ?? ''));
        $path  = $dir . '/' . $token;
        $map   = (array) ($_POST['map'] ?? []);

        if ($token === '' || !is_file($path)) {
            return ['source', 'The uploaded data expired — please start again.', null];
        }
        if (Events::fire("tables.$table.page.beforeImport", ['file' => $path, 'map' => $map]) === false) {
            @unlink($path);
            return ['source', 'Import was cancelled by a business rule.', null];
        }

        [$header, $rows] = ImportRunner::read($table, $path, (string) ($_POST['reader'] ?? 'csv'), [
            'hasHeader' => !empty($_POST['has_header']),
            'delimiter' => (string) ($_POST['delimiter'] ?? 'auto'),
        ]);
        [$prepared, $errors] = ImportRunner::prepare($table, $rows, $map, $names);

        $mode = (string) ($_POST['mode'] ?? 'insert');
        if (!in_array($mode, $modes, true)) $mode = 'insert';
        $matchField = (string) ($_POST['match_field'] ?? '');
        if (!in_array($matchField, $names, true)) $matchField = '';
        if ($mode !== 'insert' && $matchField === '') $mode = 'insert';

        // "Validate before import" is all-or-nothing: one bad row writes nothing.
        if (!empty($_POST['validate']) && $errors !== []) {
            @unlink($path);
            return ['done', '', ['ok' => 0, 'updated' => 0, 'skipped' => 0, 'fail' => count($errors),
                'errs' => array_slice($errors, 0, 25), 'aborted' => true]];
        }

        $result = ImportRunner::commit($table, $prepared, $mode, $matchField, $errors);
        $result['errs'] = array_slice($result['errs'], 0, 25);
        @unlink($path);
        return ['done', '', $result];
    }

    /**
     * Pick a reader for pasted text by looking at how it starts. Only readers
     * that declare supportsPaste() are eligible — a workbook is a binary ZIP and
     * can never arrive through the textarea.
     */
    private static function sniffPaste(string $text, array $readers): string
    {
        $pasteable = self::pasteableReaders($readers);
        $t = ltrim($text);
        if (in_array('json', $pasteable, true) && $t !== '' && ($t[0] === '{' || $t[0] === '[')) return 'json';
        if (in_array('xml', $pasteable, true) && str_starts_with($t, '<')) return 'xml';
        return in_array('csv', $pasteable, true) ? 'csv' : ($pasteable[0] ?? 'csv');
    }

    /** @return list<string> the configured readers that accept pasted text */
    private static function pasteableReaders(array $readers): array
    {
        $all = FormatRegistry::readers();
        return array_values(array_filter($readers, static fn(string $id): bool => isset($all[$id]) && $all[$id]::supportsPaste()));
    }

    // ── view ────────────────────────────────────────────────────────────────

    private static function render(array $v): void
    {
        extract($v);
        /** @var array<string,string> $fields */
        $crumb = '<nav class="pp-breadcrumb"><a href="' . APP_BASE . '/">Home</a>'
            . '<i class="bi bi-chevron-right sep"></i><a href="' . APP_BASE . '/generated/' . $table . '/list.php">' . htmlspecialchars($caption) . '</a>'
            . '<i class="bi bi-chevron-right sep"></i><span>Import</span></nav>';

        $head = '';
        foreach ((array) ($cfg['css'] ?? []) as $href) {
            $head .= '<link rel="stylesheet" href="' . APP_BASE . '/' . htmlspecialchars((string) $href) . '">';
        }
        echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
            . '<meta name="viewport" content="width=device-width,initial-scale=1">'
            . '<title>' . htmlspecialchars($caption) . ' — Import</title>'
            . $head . '</head>'
            . '<body data-pp-table="' . htmlspecialchars($table) . '" data-pp-action="import">';

        pp_shell_start(pp_page_title($table, 'import', $caption . ' — Import'), $table, $crumb);

        echo '<div class="pp-import">';

        // The designed page (Designer → Import) supplies block order, presence
        // and labels; the runtime supplies the live widget for each slot. With
        // no design we fall back to the standard wizard layout.
        $blocks = (array) ($cfg['blocks'] ?? []);
        if ($blocks === []) {
            $blocks = [
                ['type' => 'steps', 'label' => 'Source|Mapping|Preview|Import'],
                ['type' => 'text', 'label' => ''],
                ['type' => 'source', 'label' => ''],
                ['type' => 'options', 'label' => ''],
                ['type' => 'mapper', 'label' => ''],
                ['type' => 'preview', 'label' => ''],
                ['type' => 'summary', 'label' => ''],
                ['type' => 'buttons', 'label' => ''],
            ];
        }
        $v['blocks'] = $blocks;

        if ($error !== '') {
            $v['__errorShown'] = true;
        }
        self::renderBlocks($v);

        echo '</div>';
        pp_shell_end();
        echo (string) ($cfg['chrome'] ?? '');
        echo '<script>window.PP_BASE = "' . APP_BASE . '";</script>';
        echo self::script();
        echo '<script src="' . APP_BASE . '/assets/jquery/jquery.min.js"></script>'
            . '<script src="' . APP_BASE . '/assets/js/app.js"></script>'
            . '</body></html>';
    }

    /**
     * Walk the block list for the current step. The design decides which blocks
     * appear and in what order; each step owns its <form> wrapper because the
     * wizard posts as a unit.
     */
    private static function renderBlocks(array $v): void
    {
        $step = $v['step'];
        $order = array_column($v['blocks'], 'type');
        $labels = [];
        foreach ($v['blocks'] as $b) $labels[$b['type']] = $b['label'];
        $v['labels'] = $labels;
        $has = static fn(string $t): bool => in_array($t, $order, true);

        // Progress bar first wherever it is declared.
        if ($has('steps')) self::blockSteps($v);
        if (!empty($v['error'])) {
            echo '<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> ' . htmlspecialchars($v['error']) . '</div>';
        }

        if ($step === 'source') {
            if ($has('text')) self::blockLead($v);
            echo '<form method="post" enctype="multipart/form-data">' . csrfField();
            foreach ($order as $t) {
                if ($t === 'source')  self::blockSource($v);
                if ($t === 'options') self::blockOptions($v);
            }
            if (!$has('source'))  self::blockSource($v);    // a source is mandatory
            self::blockActionsSource($v);
            echo '</form>';
            return;
        }

        if ($step === 'map') {
            if ($has('summary')) self::blockTilesMap($v);
            echo '<div class="pp-record-card"><div class="pp-record-card-h"><i class="bi bi-diagram-2"></i> '
                . htmlspecialchars($labels['mapper'] ?? '3 · Field Mapping')
                . '<span style="margin-left:auto;font-weight:500;color:var(--pp-muted)">' . (int) $v['preview']['total'] . ' row(s) found</span></div>';
            echo '<form method="post" class="pp-import-body">' . csrfField();
            self::mapHiddenFields($v);
            foreach ($order as $t) {
                if ($t === 'mapper')  self::blockMapper($v);
                if ($t === 'preview') self::blockPreview($v);
            }
            if (!$has('mapper')) self::blockMapper($v);
            self::blockActionsMap($v);
            echo '</form></div>';
            return;
        }

        if ($step === 'done') self::blockDone($v);
    }

    private static function blockSteps(array $v): void
    {
        $labels = $v['labels']['steps'] ?? '';
        $names = $labels !== '' ? explode('|', $labels) : ['Source', 'Mapping', 'Preview', 'Import'];
        $stepIdx = ['source' => 0, 'map' => 1, 'done' => count($names) - 1][$v['step']] ?? 0;
        echo '<ol class="pp-steps">';
        foreach ($names as $i => $label) {
            // On the mapping screen both Mapping and Preview are on-screen.
            $active = $i === $stepIdx || ($v['step'] === 'map' && $i === 2 && count($names) > 3);
            $cls = $active ? 'is-active' : ($i < $stepIdx ? 'is-done' : '');
            echo '<li class="' . $cls . '"><span>' . ($i + 1) . '</span> ' . htmlspecialchars($label) . '</li>';
        }
        echo '</ol>';
    }

    private static function blockLead(array $v): void
    {
        $lead = $v['labels']['text'] ?? '';
        if ($lead === '') $lead = (string) ($v['opt']['lead'] ?? 'Upload a file or paste rows copied from Excel, CSV, or another source.');
        echo '<p class="pp-import-lead">' . htmlspecialchars($lead) . '</p>';
    }

    private static function blockSource(array $v): void
    {
        extract($v);
        $canPaste = $allowPaste && self::pasteableReaders($readers) !== [];
        $sources = [];
        if ($canPaste)    $sources['paste'] = 'Copy / Paste';
        if ($allowUpload) $sources['file']  = 'Upload File';
        if ($sources === []) $sources['file'] = 'Upload File';
        $first = array_key_first($sources);

        echo '<div class="pp-record-card"><div class="pp-record-card-h"><i class="bi bi-box-arrow-in-down"></i> '
            . htmlspecialchars($labels['source'] ?? '1 · Import Source') . '</div><div class="pp-import-body">';
        if (count($sources) > 1) {
            echo '<div class="pp-imp-tabs" role="tablist">';
            foreach ($sources as $k => $label) {
                echo '<button type="button" class="pp-imp-tab' . ($k === $first ? ' is-active' : '') . '" data-pp-tab="' . $k . '" role="tab">' . htmlspecialchars($label) . '</button>';
            }
            echo '</div>';
        }
        if (isset($sources['paste'])) {
            echo '<div class="pp-imp-pane' . ($first === 'paste' ? ' is-active' : '') . '" data-pp-pane="paste">'
                . '<label style="display:block;font-size:.78rem;font-weight:600;margin-bottom:.35rem">Paste rows from Excel / CSV / JSON / XML</label>'
                . '<textarea name="paste" class="pp-paste" placeholder="Paste tab, comma or semicolon separated rows…"></textarea></div>';
        }
        if (isset($sources['file'])) {
            echo '<div class="pp-imp-pane' . ($first === 'file' ? ' is-active' : '') . '" data-pp-pane="file">'
                . '<label class="pp-drop"><input type="file" name="file" accept="' . htmlspecialchars($accept) . '">'
                . '<span class="pp-drop-inner"><i class="bi bi-cloud-arrow-up"></i><b>Choose a file</b>'
                . '<small>or drag it here · ' . htmlspecialchars($extLbl) . ' · max ' . (int) $maxMb . ' MB</small></span></label></div>';
        }
        echo '<div class="pp-import-actions"><a href="' . APP_BASE . '/generated/' . $table . '/import.php?sample=csv" class="ppd-grid-toolbar-btn"><i class="bi bi-download"></i> Download Sample</a></div>';
        echo '</div></div>';
    }

    private static function blockOptions(array $v): void
    {
        extract($v);
        echo '<div class="pp-record-card" style="margin-top:1rem"><div class="pp-record-card-h"><i class="bi bi-sliders"></i> '
            . htmlspecialchars($labels['options'] ?? '2 · Options') . '</div><div class="pp-import-body"><div class="pp-opt-grid">';

        echo '<label><span>Delimiter</span><select name="delimiter" class="ppd-grid-search">';
        foreach (['auto' => 'Detect automatically', 'comma' => 'Comma ( , )', 'semicolon' => 'Semicolon ( ; )', 'tab' => 'Tab', 'pipe' => 'Pipe ( | )'] as $k => $label) {
            echo '<option value="' . $k . '"' . (($opt['delimiter'] ?? 'auto') === $k ? ' selected' : '') . '>' . $label . '</option>';
        }
        echo '</select></label>';

        if (count($modes) > 1) {
            $modeLabels = ['insert' => 'Insert all rows', 'skip' => 'Skip rows that already exist', 'update' => 'Update rows that already exist'];
            echo '<label><span>Import mode</span><select name="mode" class="ppd-grid-search">';
            foreach ($modes as $m) echo '<option value="' . $m . '">' . $modeLabels[$m] . '</option>';
            echo '</select></label>';
            echo '<label><span>Match existing on</span><select name="match_field" class="ppd-grid-search">';
            foreach ($fields as $fn => $fl) {
                echo '<option value="' . htmlspecialchars($fn) . '"' . ($fn === $matchDefault ? ' selected' : '') . '>' . htmlspecialchars($fl) . '</option>';
            }
            echo '</select></label>';
        }

        echo '<div style="display:flex;flex-direction:column;gap:.5rem;justify-content:center">'
            . '<label class="pp-check"><input type="checkbox" name="has_header" value="1"' . ((($opt['hasHeaderDefault'] ?? true) !== false) ? ' checked' : '') . '> First row contains column headers</label>'
            . '<label class="pp-check"><input type="checkbox" name="validate" value="1"' . (!empty($opt['validateDefault']) ? ' checked' : '') . '> Validate before import (import nothing if any row fails)</label>'
            . '</div></div></div></div>';
    }

    private static function blockActionsSource(array $v): void
    {
        echo '<div class="pp-import-actions">'
            . '<button type="submit" class="ppd-auth-action" style="width:auto"><i class="bi bi-arrow-right"></i> Continue</button>'
            . '<a href="' . APP_BASE . '/generated/' . $v['table'] . '/list.php" class="ppd-grid-toolbar-btn">Cancel</a>'
            . '</div>';
    }

    private static function mapHiddenFields(array $v): void
    {
        $p = $v['preview'];
        foreach (['__do' => 'import', 'token' => $p['token'], 'has_header' => $p['hasHeader'] ? '1' : '0',
                  'delimiter' => $p['delimiter'], 'reader' => $p['reader'],
                  'mode' => $p['mode'], 'match_field' => $p['matchField']] as $k => $val) {
            echo '<input type="hidden" name="' . $k . '" value="' . htmlspecialchars((string) $val) . '">';
        }
        if (!empty($p['validate'])) echo '<input type="hidden" name="validate" value="1">';
    }

    private static function blockTilesMap(array $v): void
    {
        $mapped = 0;
        foreach ($v['fields'] as $fn => $fl) if (ImportRunner::guess($fn, $fl, $v['preview']['header']) !== '') $mapped++;
        $unmapped = count($v['fields']) - $mapped;
        echo '<div class="pp-sum-tiles">'
            . '<div class="pp-sum-tile"><b>' . $mapped . '/' . count($v['fields']) . '</b><span>Fields matched</span></div>'
            . '<div class="pp-sum-tile"><b>' . (int) $v['preview']['total'] . '</b><span>Rows found</span></div>'
            . '<div class="pp-sum-tile' . ($unmapped ? ' warn' : '') . '"><b>' . $unmapped . '</b><span>Unmapped fields</span></div>'
            . '<div class="pp-sum-tile"><b>' . htmlspecialchars(strtoupper((string) $v['preview']['reader'])) . '</b><span>'
            . ($v['preview']['source'] === 'paste' ? 'Pasted' : 'File') . '</span></div></div>';
    }

    private static function blockMapper(array $v): void
    {
        echo '<div class="pp-map-grid">';
        foreach ($v['fields'] as $fn => $fl) {
            $g = ImportRunner::guess($fn, $fl, $v['preview']['header']);
            echo '<label><span>' . htmlspecialchars($fl) . '</span><select name="map[' . htmlspecialchars($fn) . ']" class="ppd-grid-search">'
                . '<option value="">— skip —</option>';
            foreach ($v['preview']['header'] as $ci => $ch) {
                $lbl = $ch !== '' ? $ch : 'Column ' . ((int) $ci + 1);
                echo '<option value="' . (int) $ci . '"' . ((string) $ci === $g ? ' selected' : '') . '>' . htmlspecialchars((string) $lbl) . '</option>';
            }
            echo '</select></label>';
        }
        echo '</div>';
    }

    private static function blockPreview(array $v): void
    {
        $p = $v['preview'];
        echo '<div class="pp-import-preview"><div style="font-weight:700;margin:1rem 0 .5rem"><i class="bi bi-table"></i> '
            . htmlspecialchars($v['labels']['preview'] ?? '4 · Preview') . ' (first ' . count($p['rows']) . ' rows)</div>'
            . '<div style="overflow:auto"><table class="ppd-grid-table"><thead><tr>';
        foreach ($p['header'] as $ci => $ch) echo '<th>' . htmlspecialchars((string) ($ch !== '' ? $ch : 'Column ' . ((int) $ci + 1))) . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($p['rows'] as $pr) {
            echo '<tr>';
            foreach (array_keys($p['header']) as $ci) echo '<td>' . htmlspecialchars((string) ($pr[$ci] ?? '')) . '</td>';
            echo '</tr>';
        }
        echo '</tbody></table></div></div>';
    }

    private static function blockActionsMap(array $v): void
    {
        echo '<div class="pp-import-actions">'
            . '<button type="submit" class="ppd-auth-action" style="width:auto"><i class="bi bi-download"></i> Import ' . (int) $v['preview']['total'] . ' row(s)</button>'
            . '<a href="' . APP_BASE . '/generated/' . $v['table'] . '/import.php" class="ppd-grid-toolbar-btn">Start over</a>'
            . '</div>';
    }

    private static function blockDone(array $v): void
    {
        $summary = $v['summary'];
        $aborted = !empty($summary['aborted']);
        echo '<div class="pp-record-card"><div class="pp-record-card-h">'
            . ($aborted ? '<i class="bi bi-shield-exclamation"></i> Import cancelled — nothing was written'
                        : '<i class="bi bi-check2-circle"></i> Import complete')
            . '</div><div class="pp-import-body">';
        if ($aborted) {
            echo '<div class="alert alert-danger" style="margin-bottom:1rem"><i class="bi bi-exclamation-triangle"></i> '
                . '"Validate before import" was on and ' . (int) $summary['fail'] . ' row(s) failed validation, so no records were imported.</div>';
        }
        echo '<div class="pp-sum-tiles">'
            . '<div class="pp-sum-tile"><b>' . (int) $summary['ok'] . '</b><span>Imported</span></div>'
            . '<div class="pp-sum-tile"><b>' . (int) ($summary['updated'] ?? 0) . '</b><span>Updated</span></div>'
            . '<div class="pp-sum-tile' . (!empty($summary['skipped']) ? ' warn' : '') . '"><b>' . (int) ($summary['skipped'] ?? 0) . '</b><span>Skipped</span></div>'
            . '<div class="pp-sum-tile' . ($summary['fail'] ? ' err' : '') . '"><b>' . (int) $summary['fail'] . '</b><span>Failed</span></div>'
            . '</div>';
        if (!empty($summary['errs'])) {
            echo '<ul class="pp-import-errs">';
            foreach ($summary['errs'] as $e) echo '<li>' . htmlspecialchars((string) $e) . '</li>';
            echo '</ul>';
        }
        echo '<div class="pp-import-actions">'
            . '<a href="' . APP_BASE . '/generated/' . $v['table'] . '/list.php" class="ppd-auth-action" style="width:auto"><i class="bi bi-list-ul"></i> View records</a>'
            . '<a href="' . APP_BASE . '/generated/' . $v['table'] . '/import.php" class="ppd-grid-toolbar-btn">Import more</a>'
            . '</div></div></div>';
    }

    private static function script(): string
    {
        return <<<'JS'
<script>
(function () {
    var tabs = document.querySelectorAll('.pp-imp-tab');
    Array.prototype.forEach.call(tabs, function (t) {
        t.addEventListener('click', function () {
            var want = t.getAttribute('data-pp-tab');
            Array.prototype.forEach.call(tabs, function (o) { o.classList.toggle('is-active', o === t); });
            Array.prototype.forEach.call(document.querySelectorAll('.pp-imp-pane'), function (p) {
                p.classList.toggle('is-active', p.getAttribute('data-pp-pane') === want);
            });
            // Only one source may be submitted — clear the other.
            var ta = document.querySelector('.pp-paste'), fi = document.querySelector('.pp-drop input[type=file]');
            if (want === 'file' && ta) ta.value = '';
            if (want === 'paste' && fi) fi.value = '';
        });
    });
    var drop = document.querySelector('.pp-drop');
    if (drop) {
        ['dragenter', 'dragover'].forEach(function (e) { drop.addEventListener(e, function (ev) { ev.preventDefault(); drop.classList.add('drag'); }); });
        ['dragleave', 'drop'].forEach(function (e) { drop.addEventListener(e, function (ev) { ev.preventDefault(); drop.classList.remove('drag'); }); });
        drop.addEventListener('drop', function (ev) {
            var f = ev.dataTransfer && ev.dataTransfer.files, input = drop.querySelector('input[type=file]');
            if (f && f.length && input) input.files = f;
        });
    }
})();
</script>
JS;
    }
}
