<?php

declare(strict_types=1);

namespace PpPhp\Core\DataOps;

use PpPhp\Core\DB;
use PpPhp\Core\Security;

/**
 * The generated Export screen: Records → Fields → Format → Export.
 *
 * export.php serves two jobs from one endpoint:
 *   • `?format=…`  → an immediate download (the list toolbar menu and the
 *     "export selected" button link straight here — unchanged behaviour).
 *   • no format    → this page, so the Export page-type is a real screen where
 *     the user picks scope, columns and format before downloading.
 *
 * Like ImportPage, the layout comes from the Designer tree (block order,
 * presence and labels) while the runtime supplies each live widget.
 */
final class ExportPage
{
    /**
     * @param array{
     *   table:string, columns:array<string,string>, allowed:list<string>,
     *   caption?:string, chrome?:string, css?:list<string>, blocks?:list<array{type:string,label:string}>,
     *   lookups?:array<string,mixed>, includeHeaders?:bool
     * } $cfg
     */
    /** @var array<int, array<string, mixed>> the page's WHERE-tab definitions */
    private static array $whereTabs = [];

    public static function handle(array $cfg): void
    {
        $table   = (string) $cfg['table'];
        $columns = (array) $cfg['columns'];
        $allowed = array_values((array) ($cfg['allowed'] ?? []));
        self::$whereTabs = (array) ($cfg['whereTabs'] ?? []);   // honour the active WHERE tab on export

        if ($allowed === []) {
            http_response_code(403);
            exit('Export is not permitted for this table.');
        }

        // ── Direct download path (toolbar links, export-selected) ──
        $isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
        if (!$isPost && isset($_GET['format'])) {
            ExportRunner::handle($cfg + self::scopeQuery($table, array_keys($columns), 'filtered'));
            return;   // ExportRunner exits
        }

        // ── Submitted from this page ──
        if ($isPost) {
            if (!Security::CSRF()->Check($_POST['_token'] ?? '')) {
                self::render($cfg, 'Invalid session token — please retry.');
                return;
            }
            $format = (string) ($_POST['format'] ?? '');
            if (!in_array($format, $allowed, true)) {
                self::render($cfg, 'Choose a format to export.');
                return;
            }
            // Only the ticked columns, in the table's own order.
            $picked = array_values(array_intersect(array_keys($columns), (array) ($_POST['fields'] ?? [])));
            if ($picked === []) {
                self::render($cfg, 'Select at least one field to export.');
                return;
            }
            $chosen = [];
            foreach ($picked as $c) $chosen[$c] = $columns[$c];

            ExportRunner::handle(array_merge(
                $cfg,
                ['columns' => $chosen, 'format' => $format, 'includeHeaders' => !empty($_POST['headers'])],
                self::scopeQuery($table, array_keys($columns), (string) ($_POST['scope'] ?? 'filtered'))
            ));
            return;
        }

        self::render($cfg, '');
    }

    /**
     * Translate a scope choice into where/params/limit for ExportRunner.
     *
     * filtered — the list's current search + column filters (the default, and
     *            what the toolbar links have always done)
     * selected — only ?ids=
     * page     — the list's current slice
     * all      — everything, ignoring filters
     *
     * @return array{where:string,params:list<mixed>,limit:int,offset:int}
     */
    private static function scopeQuery(string $table, array $validFields, string $scope): array
    {
        // On submit the list context arrives as the form's hidden inputs, so POST
        // must win; a direct ?format= download only has the query string. Reading
        // just $_GET silently ignored the selection on every POST.
        $src = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' ? ($_POST + $_GET) : $_GET;

        if ($scope === 'all') {
            $src = array_diff_key($src, array_flip(['search', 'f', 'ids']));
        } elseif ($scope === 'selected') {
            unset($src['search'], $src['f']);          // the tick list IS the whole filter
        } else {
            // "filtered" and "page" describe the list's own view — a leftover
            // selection must not narrow them further.
            unset($src['ids']);
        }
        [$where, $params] = ExportRunner::buildWhere($table, $validFields, $src);

        // Honour the active Additional-WHERE tab (unless exporting a fixed set or
        // "all") so a "This month" / ":userid" tab carries into the export. SQL
        // variables are expanded here too.
        if ($scope !== 'selected' && $scope !== 'all' && self::$whereTabs !== []) {
            $wtab = isset($src['wtab']) && $src['wtab'] !== '' ? (int) $src['wtab'] : -1;
            $clause = $wtab >= 0 ? trim((string) (self::$whereTabs[$wtab]['clause'] ?? '')) : '';
            if ($clause !== '') {
                if (class_exists('\PpPhp\Core\SqlVars')) {
                    $clause = \PpPhp\Core\SqlVars::expand($clause);
                }
                $where = $where === '' ? 'WHERE (' . $clause . ')' : $where . ' AND (' . $clause . ')';
            }
        }

        // Row-Level Security applies to exports too — a user can only export the
        // rows they are allowed to see.
        if (class_exists('\PpPhp\Core\Auth\RowSecurity')) {
            $where = \PpPhp\Core\Auth\RowSecurity::mergeWhere($where, $table);
        }

        $limit = 0; $offset = 0;
        if ($scope === 'page') {
            $limit  = max(1, (int) ($src['perPage'] ?? 10));
            $offset = (max(1, (int) ($src['page'] ?? 1)) - 1) * $limit;
        }
        return ['where' => $where, 'params' => $params, 'limit' => $limit, 'offset' => $offset];
    }

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

    private static function render(array $cfg, string $error): void
    {
        $table   = (string) $cfg['table'];
        $columns = (array) $cfg['columns'];
        $allowed = array_values((array) ($cfg['allowed'] ?? []));
        $caption = (string) ($cfg['caption'] ?? $table);

        $blocks = (array) ($cfg['blocks'] ?? []);
        if ($blocks === []) {
            $blocks = [
                ['type' => 'steps', 'label' => 'Records|Fields|Format|Export'],
                ['type' => 'text', 'label' => ''],
                ['type' => 'scope', 'label' => ''],
                ['type' => 'fields', 'label' => ''],
                ['type' => 'format', 'label' => ''],
                ['type' => 'preview', 'label' => ''],
                ['type' => 'summary', 'label' => ''],
                ['type' => 'buttons', 'label' => ''],
            ];
        }
        $labels = [];
        foreach ($blocks as $b) $labels[$b['type']] = $b['label'];
        $order = array_column($blocks, 'type');
        $has = static fn(string $t): bool => in_array($t, $order, true);

        // Counts for the tiles + preview rows, using the default (filtered) scope.
        $sq = self::scopeQuery($table, array_keys($columns), 'filtered');
        $total = (int) (DB::FetchAll("SELECT COUNT(*) AS c FROM `$table` {$sq['where']}", $sq['params'])[0]['c'] ?? 0);
        // Preview ordered by the real PK — `ORDER BY \`id\`` threw (→ empty preview)
        // on tables whose PK ≠ `id` (e.g. std_id), matching the download bug.
        $__pk = function_exists('pp_pk') ? preg_replace('/[^A-Za-z0-9_]/', '', (string) pp_pk($table)) : 'id';
        if ($__pk === '') $__pk = 'id';
        $sample = DB::FetchAll("SELECT * FROM `$table` {$sq['where']} ORDER BY `$__pk` ASC LIMIT 8", $sq['params']) ?: [];
        if (!empty($cfg['lookups']) && function_exists('pp_apply_lookups')) {
            pp_apply_lookups($sample, $cfg['lookups']);
        }
        if (!empty($cfg['optionMaps']) && function_exists('pp_apply_option_maps')) {
            pp_apply_option_maps($sample, $cfg['optionMaps']);
        }
        // Rows ticked on the list arrive as ?ids= — when they do, "Selected
        // records" is the scope the user already expressed, so it is pre-chosen.
        $selCount = count(array_filter(array_map('intval', explode(',', (string) ($_GET['ids'] ?? '')))));
        $hasIds = $selCount > 0;

        $head = '';
        foreach ((array) ($cfg['css'] ?? []) as $href) {
            $head .= '<link rel="stylesheet" href="' . APP_BASE . '/' . htmlspecialchars((string) $href) . '">';
        }
        $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>Export</span></nav>';

        echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
            . '<meta name="viewport" content="width=device-width,initial-scale=1">'
            . '<title>' . htmlspecialchars($caption) . ' — Export</title>' . $head . '</head>'
            . '<body data-pp-table="' . htmlspecialchars($table) . '" data-pp-action="export">';
        pp_shell_start(pp_page_title($table, 'export', $caption . ' — Export'), $table, $crumb);
        echo '<div class="pp-import">';

        if ($has('steps')) {
            $names = ($labels['steps'] ?? '') !== '' ? explode('|', $labels['steps']) : ['Records', 'Fields', 'Format', 'Export'];
            echo '<ol class="pp-steps">';
            foreach ($names as $i => $n) {
                echo '<li class="' . ($i < 3 ? 'is-active' : '') . '"><span>' . ($i + 1) . '</span> ' . htmlspecialchars($n) . '</li>';
            }
            echo '</ol>';
        }
        if ($error !== '') {
            echo '<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> ' . htmlspecialchars($error) . '</div>';
        }
        if ($has('text')) {
            $lead = $labels['text'] !== '' ? $labels['text'] : 'Choose records, fields, and format before exporting your data.';
            echo '<p class="pp-import-lead">' . htmlspecialchars($lead) . '</p>';
        }

        echo '<form method="post">' . csrfField();
        // Carry the list's filters/selection through the POST.
        foreach (['search' => (string) ($_GET['search'] ?? ''), 'ids' => (string) ($_GET['ids'] ?? ''),
                  'page' => (string) ($_GET['page'] ?? ''), 'perPage' => (string) ($_GET['perPage'] ?? '')] as $k => $val) {
            if ($val !== '') echo '<input type="hidden" name="' . $k . '" value="' . htmlspecialchars($val) . '">';
        }
        foreach ((array) ($_GET['f'] ?? []) as $fk => $fv) {
            if (trim((string) $fv) === '') continue;
            echo '<input type="hidden" name="f[' . htmlspecialchars((string) $fk) . ']" value="' . htmlspecialchars((string) $fv) . '">';
        }

        foreach ($order as $t) {
            if ($t === 'scope')   self::blockScope($labels, $hasIds, $total, $selCount);
            if ($t === 'fields')  self::blockFields($labels, $columns);
            if ($t === 'format')  self::blockFormat($labels, $allowed);
            if ($t === 'preview') self::blockPreview($labels, $columns, $sample);
            if ($t === 'summary') self::blockSummary($columns, $total);
        }
        if (!$has('format')) self::blockFormat($labels, $allowed);   // a format is mandatory

        echo '<div class="pp-import-actions">'
            . '<button type="submit" class="ppd-auth-action" style="width:auto"><i class="bi bi-download"></i> Export</button>'
            . '<a href="' . APP_BASE . '/generated/' . $table . '/list.php" class="ppd-grid-toolbar-btn">Cancel</a>'
            . '</div></form>';

        echo '</div>';
        pp_shell_end();
        echo (string) ($cfg['chrome'] ?? '');
        echo '<script>window.PP_BASE = "' . APP_BASE . '";</script>'
            . '<script>(function(){var a=document.getElementById("ppExpAll"),n=document.getElementById("ppExpNone");'
            . 'function set(v){document.querySelectorAll(".pp-exp-field").forEach(function(c){c.checked=v;});}'
            . 'if(a)a.addEventListener("click",function(e){e.preventDefault();set(true);});'
            . 'if(n)n.addEventListener("click",function(e){e.preventDefault();set(false);});})();</script>'
            . '<script src="' . APP_BASE . '/assets/jquery/jquery.min.js"></script>'
            . '<script src="' . APP_BASE . '/assets/js/app.js"></script></body></html>';
    }

    private static function blockScope(array $labels, bool $hasIds, int $total, int $selCount = 0): void
    {
        $opts = [
            'selected' => $hasIds
                ? 'Selected records — the ' . number_format($selCount) . ' row(s) you ticked on the list'
                : 'Selected records (none were selected)',
            'filtered' => 'Filtered records — everything matching the current search and filters',
            'page'     => 'Current page only',
            'all'      => 'All records — ignore filters',
        ];
        // A selection is an explicit choice, so it wins the default.
        $default = $hasIds ? 'selected' : 'filtered';

        echo '<div class="pp-record-card"><div class="pp-record-card-h"><i class="bi bi-collection"></i> '
            . htmlspecialchars($labels['scope'] ?? '1 · Records') . '</div><div class="pp-import-body">';
        foreach ($opts as $k => $label) {
            $dis = ($k === 'selected' && !$hasIds) ? ' disabled' : '';
            echo '<label class="pp-check" style="display:flex;margin-top:.35rem">'
                . '<input type="radio" name="scope" value="' . $k . '"' . ($k === $default ? ' checked' : '') . $dis . '> '
                . htmlspecialchars($label) . '</label>';
        }
        echo '<p class="text-muted" style="margin:.7rem 0 0;font-size:.82rem;color:var(--pp-muted)">'
            . number_format($total) . ' record(s) match the current filters.</p>';
        echo '</div></div>';
    }

    private static function blockFields(array $labels, array $columns): void
    {
        echo '<div class="pp-record-card" style="margin-top:1rem"><div class="pp-record-card-h"><i class="bi bi-layout-three-columns"></i> '
            . htmlspecialchars($labels['fields'] ?? '2 · Fields')
            . '<span style="margin-left:auto;font-weight:500">'
            . '<a href="#" id="ppExpAll" class="ppd-grid-toolbar-btn" style="padding:.15rem .5rem">All</a> '
            . '<a href="#" id="ppExpNone" class="ppd-grid-toolbar-btn" style="padding:.15rem .5rem">None</a></span></div>'
            . '<div class="pp-import-body"><div class="pp-opt-grid">';
        foreach ($columns as $name => $label) {
            echo '<label class="pp-check"><input class="pp-exp-field" type="checkbox" name="fields[]" value="'
                . htmlspecialchars($name) . '" checked> ' . htmlspecialchars($label) . '</label>';
        }
        echo '</div></div></div>';
    }

    private static function blockFormat(array $labels, array $allowed): void
    {
        echo '<div class="pp-record-card" style="margin-top:1rem"><div class="pp-record-card-h"><i class="bi bi-filetype-doc"></i> '
            . htmlspecialchars($labels['format'] ?? '3 · Format') . '</div><div class="pp-import-body"><div class="pp-opt-grid">';
        $reg = FormatRegistry::exports();
        $first = true;
        foreach ($allowed as $id) {
            $cls = $reg[$id] ?? null;
            if ($cls === null) continue;
            [$icon, $color] = $cls::icon();
            echo '<label class="pp-check"><input type="radio" name="format" value="' . htmlspecialchars($id) . '"'
                . ($first ? ' checked' : '') . '> <i class="bi ' . htmlspecialchars($icon) . '" style="color:' . htmlspecialchars($color) . '"></i> '
                . htmlspecialchars($cls::label()) . ' <span style="color:var(--pp-muted)">.' . htmlspecialchars($cls::extension()) . '</span></label>';
            $first = false;
        }
        echo '</div><label class="pp-check" style="margin-top:.8rem"><input type="checkbox" name="headers" value="1" checked> Include the header row</label>';
        echo '</div></div>';
    }

    private static function blockPreview(array $labels, array $columns, array $sample): void
    {
        echo '<div class="pp-record-card" style="margin-top:1rem"><div class="pp-record-card-h"><i class="bi bi-table"></i> '
            . htmlspecialchars($labels['preview'] ?? '4 · Preview') . '</div><div class="pp-import-body">';
        if ($sample === []) {
            echo '<p style="color:var(--pp-muted);margin:0">No records match the current filters.</p>';
        } else {
            echo '<div style="overflow:auto"><table class="ppd-grid-table"><thead><tr>';
            foreach ($columns as $label) echo '<th>' . htmlspecialchars((string) $label) . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($sample as $r) {
                echo '<tr>';
                foreach (array_keys($columns) as $c) echo '<td>' . htmlspecialchars((string) ($r[$c] ?? '')) . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table></div>';
        }
        echo '</div></div>';
    }

    private static function blockSummary(array $columns, int $total): void
    {
        echo '<div class="pp-sum-tiles" style="margin-top:1rem">'
            . '<div class="pp-sum-tile"><b>' . count($columns) . '</b><span>Fields available</span></div>'
            . '<div class="pp-sum-tile"><b>' . number_format($total) . '</b><span>Rows matched</span></div>'
            . '</div>';
    }
}
