<?php
declare(strict_types=1);

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

if (!defined('INSTINCTOR_NET_FREE_CARDS')) {
    // Free WordPress tier: max Network cards shown across the WHOLE site.
    define('INSTINCTOR_NET_FREE_CARDS', 3);
}

/**
 * Free-tier site-wide Network card budget.
 *
 * The Network block itself is free, but the free WordPress tier may show at most
 * INSTINCTOR_NET_FREE_CARDS cards TOTAL across every published page — not per
 * block — so a handful of small networks can't be stitched into one big system.
 * Cards are allocated greedily in a canonical order (page id, then element order
 * within the page); this returns how many cards the given network element may
 * render. Result is memoised for the request. Standalone, the editor, and
 * licensed Pro never call this (they are unlimited).
 *
 * @param string $pageId    Id of the page this network is being rendered on.
 * @param string $elId      The network element's id (unique only within a page).
 * @param int    $cardCount Cards this network would render uncapped (fallback).
 * @return int Number of cards this network may render under the site budget.
 */
function impulse_network_free_allotment(string $pageId, string $elId, int $cardCount): int {
    static $alloc = null;
    $budget = (int) INSTINCTOR_NET_FREE_CARDS;
    if ($alloc === null) {
        $alloc = [];
        $remaining = $budget;
        try {
            if (class_exists('\\Instinctor\\Core\\DB')) {
                $t    = \Instinctor\Core\DB::prefix() . 'instinctor_pages';
                // Only published pages count toward the budget; drafts/trash never
                // render publicly so they must not starve a live network.
                $rows = \Instinctor\Core\DB::get_results(
                    "SELECT id, elements FROM {$t} WHERE status = 'published' AND elements LIKE '%network%' ORDER BY id ASC"
                );
                foreach ((array) $rows as $row) {
                    $pid = (string) ($row->id ?? '');
                    $els = json_decode((string) ($row->elements ?? ''), true);
                    if (!is_array($els)) continue;
                    foreach ($els as $e) {
                        if (!is_array($e) || ($e['type'] ?? '') !== 'network') continue;
                        $eid = (string) ($e['id'] ?? '');
                        if ($eid === '') continue;
                        // Element ids are unique only within a page (copying a page
                        // duplicates them), so key the budget by page id + element id.
                        $key = $pid . ':' . $eid;
                        if (isset($alloc[$key])) continue;
                        $s = $e['settings'] ?? [];
                        if (is_string($s)) $s = json_decode($s, true);
                        $cs = is_array($s) ? ($s['cards'] ?? null) : null;
                        if (is_string($cs)) $cs = json_decode($cs, true);
                        $n = is_array($cs) ? count($cs) : 0;
                        $give = max(0, min($n, $remaining));
                        $alloc[$key] = $give;
                        $remaining  -= $give;
                    }
                }
            }
        } catch (\Throwable $e) {
            $alloc = []; // fall through to the per-block fallback below
        }
    }
    $key = $pageId . ':' . $elId;
    if (array_key_exists($key, $alloc)) return $alloc[$key];
    // Unknown page/element (e.g. current-page global missing): cap to the raw
    // budget so we never over-show.
    return max(0, min($cardCount, $budget));
}

/**
 * Network block renderer (shared frontend + editor).
 *
 * Animated capability grid: icon cards on an even grid connected by routes
 * (ordered card sequences). A light trace travels each route, lighting each
 * card as it arrives and flowing onward as it leaves. Hover reveals a quick
 * description; click opens a modal with an optional CTA. The geometry +
 * animation live in assets/js/impulse-network.js; styles in
 * assets/css/impulse-network.css.
 *
 * Settings (stored on the element):
 *   cols, rows        grid dimensions (default 4 x 2)
 *   gap, cell_size    spacing + card size in px (default 16 / 84)
 *   rotation_speed    seconds per gradient-fill rotation (default 5)
 *   travel_speed      px per second the trace travels (default 300)
 *   hold              ms the trace holds at full extension (default 950)
 *   idle              'cycle' (auto) | 'static' (dark until hover)
 *   color1..color4    palette swatches (default cyan -> violet)
 *   card_bg           idle card background (default #0e1426)
 *   cards             JSON array: [{id,icon,label,desc,link,col,row}, ...]
 *   routes            JSON array of ordered card-id lists: [["a","b"], ...]
 *
 * Pro feature 'network' = an UNLIMITED network. The block animates for everyone;
 * standalone + admin/editor + an active licence show unlimited cards, while a
 * free WordPress frontend is capped to INSTINCTOR_NET_FREE_CARDS cards total
 * across the whole site (see impulse_network_free_allotment).
 *
 * @param array|object $el       Element node.
 * @param array        $settings Resolved settings (editor) or raw (frontend).
 * @param string       $context  'frontend' or 'editor'.
 *
 * @return string Inner HTML.
 *
 * @sideeffect Frontend enqueues the instinctor-network js + css handles.
 */
function impulse_element_render_network($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $isEditor = ($context === 'editor');

    $cols   = max(1, min(12, intval($settings['cols'] ?? 4)));
    $rows   = max(1, min(12, intval($settings['rows'] ?? 2)));

    // Per-device grid dims (blank = inherit desktop) + per-device card-position
    // override maps {id:[col,row]}. The engine resolves both by breakpoint.
    $dimDev = function ($key) use ($settings) {
        $v = $settings[$key] ?? '';
        return ($v === '' || $v === null) ? null : max(1, min(12, intval($v)));
    };
    $colsT = $dimDev('cols_tablet'); $colsM = $dimDev('cols_mobile');
    $rowsT = $dimDev('rows_tablet'); $rowsM = $dimDev('rows_mobile');
    $posT  = $settings['positions_tablet'] ?? '';
    $posM  = $settings['positions_mobile'] ?? '';
    if (is_array($posT)) $posT = function_exists('wp_json_encode') ? wp_json_encode($posT) : json_encode($posT);
    if (is_array($posM)) $posM = function_exists('wp_json_encode') ? wp_json_encode($posM) : json_encode($posM);
    $renderDevice = $isEditor ? (string) ($GLOBALS['impulse_preview_device'] ?? 'desktop') : '';

    $gap    = max(0, intval($settings['gap'] ?? 16));
    $cell   = max(40, intval($settings['cell_size'] ?? 84));
    $rot    = (float) ($settings['rotation_speed'] ?? 5);
    if ($rot <= 0) $rot = 5;
    $travel = max(40, intval($settings['travel_speed'] ?? 300));
    $hold   = max(0, intval($settings['hold'] ?? 950));
    // Start delay: ms the line animation is held after the screen reveals, so
    // the card decompose can finish before traces begin. Default 1800 keeps
    // existing pages unchanged; 0 starts the animation immediately.
    $revealHold = max(0, intval($settings['reveal_hold'] ?? 1800));
    $idle   = (($settings['idle'] ?? 'cycle') === 'static') ? 'static' : 'cycle';

    $c1     = (string) ($settings['color1'] ?? '#22d3ee');
    $c2     = (string) ($settings['color2'] ?? '#3b82f6');
    $c3     = (string) ($settings['color3'] ?? '#8b5cf6');
    $c4     = (string) ($settings['color4'] ?? '#d946ef');
    $cardBg = (string) ($settings['card_bg'] ?? '#0e1426');
    // Line colours: empty = inherit the card gradient (the engine reads the
    // --net-line-c* vars and falls back to the c1..c4 stops). Card border:
    // empty = the stylesheet default. Fill motion picks the lit-card animation.
    $lineC1     = (string) ($settings['line_c1'] ?? '');
    $lineC2     = (string) ($settings['line_c2'] ?? '');
    $cardBorder = (string) ($settings['card_border'] ?? '');
    $fillMotion = (string) ($settings['fill_motion'] ?? 'spin');
    if (!in_array($fillMotion, ['spin', 'sweep', 'pulse', 'off'], true)) $fillMotion = 'spin';

    // Cards + routes stored as JSON strings, mirroring the contact `fields` pattern.
    $cards = $settings['cards'] ?? null;
    if (is_string($cards)) $cards = json_decode($cards, true);
    if (empty($cards) || !is_array($cards)) $cards = impulse_network_default_cards();

    // Normalise routes. A route is either a flat list of card ids (one line) or a
    // list of such lists (parallel lines that animate together). The legacy
    // [{cards:[...]}] shape is still accepted. Single-line routes stay flat;
    // multi-line routes keep their nesting. Any line with < 2 valid cards is dropped.
    // Returns null when the input isn't a usable array (so per-device callers can
    // tell "no override" from "an empty override").
    $normRoutes = function ($routes) {
        if (is_string($routes)) $routes = json_decode($routes, true);
        if (!is_array($routes)) return null;
        $out = [];
        foreach ($routes as $r) {
            if (is_array($r) && isset($r['cards'])) $r = $r['cards'];
            if (!is_array($r) || !count($r)) continue;
            $isMulti = isset($r[0]) && is_array($r[0]);
            $lines   = $isMulti ? $r : [$r];
            $clean   = [];
            foreach ($lines as $line) {
                if (!is_array($line)) continue;
                $line = array_values(array_filter(array_map('strval', $line), function ($s) { return $s !== ''; }));
                if (count($line) >= 2) $clean[] = $line;
            }
            if (!$clean) continue;
            $out[] = ($isMulti || count($clean) > 1) ? $clean : $clean[0];
        }
        return $out;
    };

    $routes = $settings['routes'] ?? null;
    if (is_string($routes)) $routes = json_decode($routes, true);
    if (!is_array($routes)) $routes = impulse_network_default_routes();
    $routeData = $normRoutes($routes) ?: [];

    // Per-device route overrides (blank = inherit desktop). The engine reads
    // data-routes-mobile / -tablet for the active breakpoint and falls back to
    // data-routes, so editing the line map on the mobile tab never touches desktop.
    $routeDataT = $normRoutes($settings['routes_tablet'] ?? '');
    $routeDataM = $normRoutes($settings['routes_mobile'] ?? '');

    // ── Pro gate ──────────────────────────────────────────────────
    // The block always animates (free included — the teaser has to be alive).
    // canFeature('network') is true for standalone, the editor, and licensed Pro;
    // false only on a free WordPress frontend, where the site-wide card budget
    // applies. Cards beyond the budget are dropped; routes referencing them fall
    // away in the engine (lines validate against rendered cards).
    $pro = '1';
    $netUnlimited = $isEditor || !instinctor_locked('network');
    if (!$netUnlimited) {
        $curPage   = $GLOBALS['impulse_current_page'] ?? null;
        $curPageId = is_object($curPage) ? (string) ($curPage->id ?? '')
                   : (is_array($curPage) ? (string) ($curPage['id'] ?? '') : '');
        if ($curPageId === '') $curPageId = (string) ($el['page_id'] ?? '');
        $allot = impulse_network_free_allotment($curPageId, (string) ($el['id'] ?? ''), count($cards));
        if ($allot < count($cards)) $cards = array_slice($cards, 0, max(0, $allot));
        if (empty($cards)) return '';
    }

    if (!$isEditor && class_exists('\\Instinctor\\Core\\Assets')) {
        \Instinctor\Core\Assets::enqueue('instinctor-network-css');
        \Instinctor\Core\Assets::enqueue('instinctor-network');
    }

    $style = '--net-c1:' . esc_attr($c1) . ';--net-c2:' . esc_attr($c2)
           . ';--net-c3:' . esc_attr($c3) . ';--net-c4:' . esc_attr($c4)
           . ';--net-card-bg:' . esc_attr($cardBg);
    // Leading semicolons: the base string ends without one (--net-card-bg:...),
    // so a bare append fused two declarations into one invalid token and the
    // browser dropped BOTH - line colour 1 and the card bg (white/red lines
    // rendered yellow because --net-line-c1 never computed; Sergei 2026-07-31).
    if ($lineC1 !== '')     $style .= ';--net-line-c1:' . esc_attr($lineC1);
    if ($lineC2 !== '')     $style .= ';--net-line-c2:' . esc_attr($lineC2);
    if ($cardBorder !== '') $style .= ';--net-card-border:' . esc_attr($cardBorder);

    $jenc = function ($v) { return function_exists('wp_json_encode') ? wp_json_encode($v) : json_encode($v); };
    $routesJson  = $jenc($routeData);
    $routesJsonT = (is_array($routeDataT) && count($routeDataT)) ? $jenc($routeDataT) : '';
    $routesJsonM = (is_array($routeDataM) && count($routeDataM)) ? $jenc($routeDataM) : '';

    ob_start();

    if ($isEditor && function_exists('impulse_visibility_badge')) {
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- impulse_visibility_badge() composes its markup from esc_attr()'d values internally.
        echo wp_kses(impulse_visibility_badge($settings), impulse_svg_allowed());
    }

    echo '<div class="impulse-network"'
       . ' data-cols="' . (int) $cols . '" data-rows="' . (int) $rows . '"'
       . ($colsT !== null ? ' data-cols-tablet="' . (int) $colsT . '"' : '')
       . ($colsM !== null ? ' data-cols-mobile="' . (int) $colsM . '"' : '')
       . ($rowsT !== null ? ' data-rows-tablet="' . (int) $rowsT . '"' : '')
       . ($rowsM !== null ? ' data-rows-mobile="' . (int) $rowsM . '"' : '')
       . (is_string($posT) && $posT !== '' && $posT !== '[]' ? ' data-pos-tablet="' . esc_attr($posT) . '"' : '')
       . (is_string($posM) && $posM !== '' && $posM !== '[]' ? ' data-pos-mobile="' . esc_attr($posM) . '"' : '')
       . ' data-gap="' . (int) $gap . '" data-cell="' . (int) $cell . '"'
       . ' data-rotation="' . esc_attr((string) $rot) . '" data-travel="' . (int) $travel . '"'
       . ' data-fill-motion="' . esc_attr($fillMotion) . '"'
       . ' data-hold="' . (int) $hold . '" data-reveal-hold="' . (int) $revealHold . '" data-idle="' . esc_attr($idle) . '" data-pro="' . esc_attr($pro) . '"'
       . ' data-routes="' . esc_attr((string) $routesJson) . '"'
       . ($routesJsonT !== '' ? ' data-routes-tablet="' . esc_attr((string) $routesJsonT) . '"' : '')
       . ($routesJsonM !== '' ? ' data-routes-mobile="' . esc_attr((string) $routesJsonM) . '"' : '')
       . ($renderDevice !== '' ? ' data-render-device="' . esc_attr($renderDevice) . '"' : '')
       . ($isEditor ? ' data-editor="1" data-el-id="' . esc_attr((string) ($el['id'] ?? '')) . '"' : '')
       . ' style="' . esc_attr($style) . '">';

    echo '<svg class="impulse-network-lines" preserveAspectRatio="none" aria-hidden="true"></svg>';

    foreach ($cards as $card) {
        if (!is_array($card)) continue;
        $id = (string) ($card['id'] ?? '');
        if ($id === '') continue;
        $icon  = (string) ($card['icon'] ?? 'circle');
        $label = (string) ($card['label'] ?? '');
        $desc  = (string) ($card['desc'] ?? '');
        $link  = (string) ($card['link'] ?? '');
        $col   = intval($card['col'] ?? 0);
        $row   = intval($card['row'] ?? 0);
        $svg   = function_exists('impulse_lucide_svg') ? impulse_lucide_svg($icon, 24) : '';
        echo '<div class="impulse-network-card" data-id="' . esc_attr($id) . '"'
           . ' data-col="' . (int) $col . '" data-row="' . (int) $row . '"'
           . ' data-desc="' . esc_attr($desc) . '" data-link="' . esc_attr($link) . '"'
           // The card is absolutely positioned and centered on its point via this transform.
           // data-decomp-base tells the screen decompose engine to PREPEND it so the card stays
           // centered while it flies in from depth (otherwise the depth transform replaces the
           // centering and the card snaps half its size on landing). v2.1.1995.
           . ' data-decomp-base="translate(-50%, -50%)">'
           . '<span class="impulse-network-ico">' . wp_kses($svg, impulse_svg_allowed()) . '</span>'
           . '<span class="impulse-network-label">' . esc_html($label) . '</span>'
           . '</div>';
    }

    echo '</div>';

    return (string) ob_get_clean();
}

/** Default demo cards (restaurant capability set) for a freshly-inserted block. */
function impulse_network_default_cards(): array {
    return [
        ['id' => 'google',   'icon' => 'search',         'label' => 'Google',       'desc' => 'Built-in SEO so you show up when people search.',     'link' => '', 'col' => 0, 'row' => 0],
        ['id' => 'ordering', 'icon' => 'shopping-bag',   'label' => 'Ordering',     'desc' => 'Commission-free online ordering on your own domain.',  'link' => '', 'col' => 2, 'row' => 0],
        ['id' => 'menu',     'icon' => 'clipboard-list', 'label' => 'Menu',         'desc' => 'A live menu that updates everywhere at once.',         'link' => '', 'col' => 3, 'row' => 0],
        ['id' => 'payments', 'icon' => 'credit-card',    'label' => 'Payments',     'desc' => 'Take card and digital payments right on your site.',   'link' => '', 'col' => 1, 'row' => 1],
        ['id' => 'data',     'icon' => 'database',        'label' => 'Data',         'desc' => 'Every order and customer in one place you own.',       'link' => '', 'col' => 2, 'row' => 1],
    ];
}

/** Default routes through the default cards. */
function impulse_network_default_routes(): array {
    return [
        ['google', 'ordering', 'data'],
        ['google', 'payments', 'data', 'menu'],
    ];
}
