<?php
/**
 * Team block — renders the owner's people FROM the staff table.
 *
 * Replaces the old arrangement of N fixed flip-box cards that had to be kept in step
 * with staff by a sync. That sync worked but could not win: a salon with two stylists
 * on a three-card template left an empty slot, and every new person needed a card to
 * already exist. Here the cards ARE the staff list, so 2 people render 2 cards and a
 * new hire appears the moment they are saved in /dashboard/team.
 *
 * Deliberately has no editable content: names, roles and photos live in staff, which is
 * the single source (see reference: staff -> site, one direction). The settings here are
 * presentation only. That is the whole point — two writable copies of the same faces is
 * exactly how the booking picker and the team section came to disagree.
 *
 * Owner-scoped: staff is shared storage on instinctor.com (migration 0046), so an
 * unscoped read would show one salon's people on another's site.
 *
 * Shared by the editor and the frontend, like reviews.php: one renderer, no drift.
 *
 * @package Instinctor
 */

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

/**
 * @param array|object $el       element row
 * @param array        $settings element settings
 * @param string       $context  'editor' | 'frontend'
 */
function impulse_element_render_team($el, array $settings, string $context): string
{
    if (is_object($el)) $el = (array) $el;
    $elId = (string) ($el['id'] ?? '0');

    $columns   = max(1, min(6, (int) ($settings['columns'] ?? 3)));
    // 'card' is the default because it is what every existing page already looks like:
    // a tall portrait with the name over its foot, as the flip-box cards rendered it.
    // Switching the data source must not silently redesign anyone's team section.
    $style     = ($settings['style'] ?? 'card') === 'circle' ? 'circle' : 'card';
    $shape     = ($settings['shape'] ?? 'circle') === 'square' ? 'square' : 'circle';
    $nameColor = (string) ($settings['name_color'] ?? '#1d2327');
    $roleColor = (string) ($settings['role_color'] ?? '#8a7d72');
    $gap       = max(0, min(60, (int) ($settings['gap'] ?? 22)));
    $showRole  = !isset($settings['show_role']) || !empty($settings['show_role']);
    $height    = max(120, min(720, (int) ($settings['card_height'] ?? 360)));
    $maxWidth  = max(0, (int) ($settings['max_width'] ?? 0));
    $marginTop = max(0, (int) ($settings['margin_top'] ?? 0));
    $radius    = max(0, min(40, (int) ($settings['border_radius'] ?? 14)));

    $people = impulse_team_members($settings);

    // Editor with nobody yet: show a hint rather than an invisible block, or the owner
    // cannot tell the difference between "empty" and "broken".
    if (!$people) {
        if ($context !== 'editor') return '';
        return '<div class="impulse-team-block impulse-team-empty" data-el="' . esc_attr($elId) . '"'
            . ' style="padding:34px 20px;text-align:center;color:#8a7d72;font-size:14px;border:1px dashed #d9cfc2;border-radius:12px">'
            . esc_html__('Your team shows here. Add people in your dashboard, under Your team.', 'instinctor')
            . '</div>';
    }

    // PER DEVICE. The grid this replaced carried columns_mobile=1, and dropping it put
    // three cards side by side on a phone. Defaults mirror that grid: 1 up on mobile,
    // 2 up on tablet (or fewer if the desktop ceiling is lower).
    $colsMobile = max(1, min(6, (int) ($settings['columns_mobile'] ?? 1)));
    $colsTablet = max(1, min(6, (int) ($settings['columns_tablet'] ?? min(2, $columns))));

    // NO HOLES. Never lay out more columns than there are people: 3 columns holding one
    // stylist is two columns of empty space, which is the same hole the fixed flip-box
    // cards left, just moved. One person = one card, centred, at a sane width — the
    // section reads as finished rather than half-filled. Every column count below is a
    // CEILING, not a target.
    $n     = count($people);
    $cols  = max(1, min($columns, $n));
    $colsT = max(1, min($colsTablet, $n));
    $colsM = max(1, min($colsMobile, $n));

    // width:100% is LOAD-BEARING: flip cards hold all content in absolutely
    // positioned faces, so they have zero intrinsic width - without an
    // explicit width the auto-sized centered grid collapses to its gaps
    // (grid-template-columns computed 0px 0px 0px; cards invisible on the
    // frontend, Sergei 2026-07-25).
    $wrap  = 'display:grid;width:100%;grid-template-columns:repeat(' . $cols . ',minmax(0,1fr));gap:' . $gap . 'px';
    // Cap the width by how many cards there actually are, so a lone card does not
    // stretch to a 1000px slab.
    $effMax = $maxWidth > 0 ? (int) round($maxWidth * $cols / max(1, $columns)) : 0;
    if ($effMax > 0)    $wrap .= ';max-width:' . $effMax . 'px';
    $wrap .= ';margin-left:auto;margin-right:auto';
    if ($marginTop > 0) $wrap .= ';margin-top:' . $marginTop . 'px';

    // Scoped @media, emitted with the block: an inline grid-template-columns cannot be
    // responsive on its own, and this renderer returns a string used by BOTH the editor
    // and the frontend, so a shared stylesheet would not reach the editor iframe.
    // !important because the inline desktop value above would otherwise win.
    $gid = 'imp-team-' . preg_replace('/[^A-Za-z0-9_-]/', '', $elId);
    $css = '<style>'
         . '@media(max-width:1024px){#' . $gid . '{grid-template-columns:repeat(' . $colsT . ',minmax(0,1fr))!important;max-width:none!important}}'
         . '@media(max-width:767px){#' . $gid . '{grid-template-columns:repeat(' . $colsM . ',minmax(0,1fr))!important;max-width:none!important}}'
         // Flip cards (members WITH a bio): hover on desktop, tap toggles the
         // .flipped class everywhere. Members without a bio stay static.
         . '#' . $gid . ' .impulse-team-flip{perspective:1200px;cursor:pointer}'
         . '#' . $gid . ' .itf-inner{position:relative;width:100%;height:100%;transform-style:preserve-3d;transition:transform 600ms ease-in-out}'
         . '#' . $gid . ' .impulse-team-flip:hover .itf-inner,#' . $gid . ' .impulse-team-flip.flipped .itf-inner{transform:rotateY(180deg)}'
         . '#' . $gid . ' .itf-face{position:absolute;inset:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;border-radius:inherit;overflow:hidden}'
         . '#' . $gid . ' .itf-back{transform:rotateY(180deg);background:#1c1611;color:#fff;display:flex;flex-direction:column;justify-content:flex-end;padding:20px;text-align:left}'
         . '</style>';

    // 'impulse-team-layout-card', NOT 'impulse-team-' . $style: the latter renders the
    // WRAPPER as .impulse-team-card, the same class each individual card carries, so any
    // card rule would also hit the container.
    $html = $css
          . '<div id="' . esc_attr($gid) . '" class="impulse-team-block impulse-team-layout-' . $style . '"'
          . ' data-el="' . esc_attr($elId) . '" style="' . esc_attr($wrap) . '">';

    foreach ($people as $m) {
        $name  = (string) ($m['name'] ?? '');
        $role  = (string) ($m['title'] ?? '');
        $photo = trim((string) ($m['photo_url'] ?? ''));
        $mono  = esc_html(mb_strtoupper(mb_substr($name, 0, 1)));

        if ($style === 'card') {
            // The flip-box look this replaces: tall portrait, foot gradient, name on it.
            // No photo yet -> a monogram on the same plate, never a stranger's stock face:
            // someone else's portrait under a real stylist's name is worse than none.
            $bio = trim((string) ($m['bio'] ?? ''));
            $bg = $photo !== ''
                ? 'linear-gradient(180deg,rgba(28,22,17,0.05),rgba(28,22,17,0.70)),url(' . esc_url($photo) . ') center/cover no-repeat'
                : 'linear-gradient(180deg,rgba(28,22,17,0.35),rgba(28,22,17,0.75)),#cfc4b6';
            $front = '';
            if ($photo === '') {
                $front .= '<span class="impulse-team-mono" style="position:absolute;inset:0;display:flex;align-items:center;'
                      . 'justify-content:center;color:rgba(255,255,255,0.65);font-size:64px;font-weight:700">' . $mono . '</span>';
            }
            $front .= '<div style="position:absolute;left:0;right:0;bottom:0;padding:18px 18px 20px;text-align:left">'
                  . '<div class="impulse-team-name" style="font-size:18px;font-weight:700;color:#fff;line-height:1.25">'
                  . esc_html($name) . '</div>';
            if ($showRole && $role !== '') {
                $front .= '<div class="impulse-team-role" style="margin-top:2px;font-size:13.5px;color:rgba(255,255,255,0.82)">'
                      . esc_html($role) . '</div>';
            }
            $front .= '</div>';

            if ($bio !== '') {
                // A member with a bio gets a FLIP card: front as always, the story
                // on the back. Members without one stay static - no empty backs.
                $html .= '<div class="impulse-team-card impulse-team-flip" onclick="this.classList.toggle(&#39;flipped&#39;)"'
                      . ' style="position:relative;height:' . $height . 'px;border-radius:' . $radius . 'px">'
                      . '<div class="itf-inner">'
                      . '<div class="itf-face" style="background:' . esc_attr($bg) . '">' . $front . '</div>'
                      . '<div class="itf-face itf-back">'
                      . '<div class="impulse-team-name" style="font-size:17px;font-weight:700;line-height:1.25;margin-bottom:4px">' . esc_html($name) . '</div>'
                      . ($showRole && $role !== '' ? '<div style="font-size:12.5px;color:rgba(255,255,255,0.7);margin-bottom:10px">' . esc_html($role) . '</div>' : '')
                      . '<div style="font-size:13.5px;line-height:1.55;color:rgba(255,255,255,0.9)">' . esc_html($bio) . '</div>'
                      . '</div></div></div>';
                continue;
            }

            $html .= '<div class="impulse-team-card" style="position:relative;height:' . $height . 'px;border-radius:'
                  . $radius . 'px;overflow:hidden;background:' . esc_attr($bg) . ';display:flex;align-items:flex-end">'
                  . $front . '</div>';
            continue;
        }

        // 'circle': avatar with the name underneath.
        $r = $shape === 'circle' ? '50%' : '12px';
        $face = $photo !== ''
            ? '<span class="impulse-team-face" style="display:block;width:100%;aspect-ratio:1;border-radius:' . $r
              . ';background:#efe7de url(' . esc_url($photo) . ') center/cover no-repeat"></span>'
            : '<span class="impulse-team-face impulse-team-mono" style="display:flex;align-items:center;justify-content:center;'
              . 'width:100%;aspect-ratio:1;border-radius:' . $r . ';background:#efe7de;color:#8a7d72;'
              . 'font-size:30px;font-weight:700">' . $mono . '</span>';

        $html .= '<div class="impulse-team-card" style="text-align:center">'
              . $face
              . '<div class="impulse-team-name" style="margin-top:12px;font-size:16px;font-weight:700;color:'
              . esc_attr($nameColor) . '">' . esc_html($name) . '</div>';
        if ($showRole && $role !== '') {
            $html .= '<div class="impulse-team-role" style="margin-top:3px;font-size:13.5px;color:'
                  . esc_attr($roleColor) . '">' . esc_html($role) . '</div>';
        }
        $html .= '</div>';
    }
    $html .= '</div>';

    return $html;
}

/**
 * The people this block shows: real staff if there are any, else the seeded members.
 *
 * STAFF ALWAYS WINS. The `members` setting is a PROP, not a second copy: a demo page has
 * owner_id NULL and no staff rows (a demo has no owner, and staff is keyed to one), so
 * without a seed every personalised demo would show an empty team — and the demo is the
 * entire free funnel. The moment a real salon has staff, the seed is ignored forever, so
 * an owner still has exactly one writable copy of their people. That is the property
 * this whole change exists to protect.
 *
 * Staff read is tenant-scoped: staff is one shared table on instinctor.com, so without
 * it a salon would list another salon's stylists. NULL owner (every standalone client)
 * reads as "this install's own staff", so nothing changes for them.
 */
function impulse_team_members(array $settings = []): array
{
    if (class_exists('\\Instinctor\\Core\\DB')) {
        $p = \Instinctor\Core\DB::prefix();
        $rows = \Instinctor\Core\DB::get_results(
            "SELECT name, title, bio, photo_url FROM {$p}instinctor_staff
              WHERE " . \Instinctor\Core\Tenant::scopeSql('owner_id') . " AND status = 'active'
              ORDER BY sort_order ASC, id ASC",
            'ARRAY_A'
        );
        if (is_array($rows) && $rows) return $rows;
    }

    // Seeded fallback (demos). JSON string or array, same as every other block's items.
    $seed = $settings['members'] ?? null;
    if (is_string($seed)) $seed = json_decode($seed, true);
    if (!is_array($seed)) return [];

    $out = [];
    foreach ($seed as $m) {
        if (!is_array($m)) continue;
        $name = trim((string) ($m['name'] ?? ''));
        if ($name === '') continue;
        $out[] = [
            'name'      => $name,
            // 'role' is what DemoService's generated cast calls it; accept both.
            'title'     => (string) ($m['title'] ?? $m['role'] ?? ''),
            'photo_url' => (string) ($m['photo_url'] ?? ''),
        ];
    }
    return $out;
}
