<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Support\Audit;
use App\Support\Html;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Throwable;

/**
 * Public (un-authenticated) endpoints powering the marketing site — port of the
 * PHP `home`, `inquiry` and public-services routes. No auth required.
 */
class PublicController extends Controller
{
    /** GET /api/public/home — everything the landing page renders. */
    public function home(): JsonResponse
    {
        // Seed the Panchang/Jyotish reference masters + quotes on first load
        // (idempotent — only fills empty tables).
        try {
            app(\App\Services\Reference\RefSeeder::class)->ensureSeeded();
        } catch (Throwable) {
        }

        return $this->ok([
            'settings'     => $this->settingsMap(),
            'site'         => $this->siteBlock(),
            'panchang'     => $this->panchang(),
            'services'     => $this->services(),
            'services_by_cat' => $this->servicesByCat(),
            'poojas'       => $this->poojas(),
            'day_specials' => $this->daySpecials(),
            'testimonials' => $this->testimonials(),
            'numerology'   => $this->numerologyBlock(),
            'num_features' => $this->numFeatures(),
            'quote'        => $this->todaysQuote(),
        ]);
    }

    /**
     * "Today's Special Quote" — a deterministic daily pick (stable through the
     * day) from featured quotes, falling back to all active quotes.
     */
    private function todaysQuote(): ?array
    {
        try {
            if (!Schema::hasTable('quotes')) {
                return null;
            }
            $base = DB::table('quotes')->where('is_active', 1);
            if (Schema::hasColumn('quotes', 'deleted_at')) {
                $base->whereNull('deleted_at');
            }
            $pool = (clone $base)->where('is_featured', 1)->orderBy('id')->get();
            if ($pool->isEmpty()) {
                $pool = $base->orderBy('id')->get();
            }
            if ($pool->isEmpty()) {
                return null;
            }
            $idx = (int) date('z') % $pool->count(); // day-of-year → stable per day
            $row = (array) $pool[$idx];
            return [
                'quote'      => (string) ($row['quote'] ?? ''),
                'quote_dev'  => (string) ($row['quote_dev'] ?? ''),
                'author'     => (string) ($row['author'] ?? ''),
                'author_dev' => (string) ($row['author_dev'] ?? ''),
                'category'   => (string) ($row['category'] ?? ''),
            ];
        } catch (Throwable) {
            return null;
        }
    }

    /** All site_settings as a key→value map. */
    private function settingsMap(): array
    {
        try {
            return Setting::query()->pluck('value', 'key')->map(fn ($v) => (string) $v)->all();
        } catch (Throwable) {
            return [];
        }
    }

    /** Ensure the ported panchang engine (global calc_* functions) is loaded. */
    private static function loadEngine(): void
    {
        if (!function_exists('calc_full_panchang')) {
            require_once app_path('Support/panchang_lib.php');
        }
        if (!function_exists('astro_positions')) {
            require_once app_path('Support/astro_lib.php');
        }
    }

    /**
     * "Today's Kundali" for the detail page — planetary chart cast for the
     * given date at the current local time (or noon for other dates), using the
     * configured location. Returns lagna + ayanamsa + per-planet house placement
     * for a North-Indian (bhava-fixed) chart. Additive; never throws to caller.
     */
    private function todayKundali(string $date, float $lat, float $lon, string $tz): array
    {
        try {
            self::loadEngine();
            $isToday = ($date === date('Y-m-d'));
            try {
                $tob = $isToday ? (new \DateTime('now', new \DateTimeZone($tz)))->format('H:i') : '12:00';
            } catch (Throwable) {
                $tob = '12:00';
            }
            $pos = astro_positions($date, $tob, 'vedic', $lat, $lon);

            $ascIdx = $pos['asc_sign_index'] ?? null;
            $planets = [];
            foreach (($pos['planets'] ?? []) as $name => $pl) {
                $si = (int) ($pl['sign_index'] ?? 0);
                $house = $ascIdx !== null ? ((($si - (int) $ascIdx) % 12 + 12) % 12) + 1 : null;
                $planets[] = [
                    'name'       => $name,
                    'abbr'       => self::planetAbbr($name),
                    'sign'       => $pl['sign'] ?? '',
                    'sign_index' => $si,
                    'house'      => $house,
                    'retro'      => (bool) ($pl['retro'] ?? false),
                ];
            }

            return [
                'available'      => $ascIdx !== null,
                'time'           => $tob,
                'ayanamsa'       => $pos['ayanamsa'] ?? null,
                'asc_sign'       => $pos['asc_sign'] ?? null,
                'asc_sign_index' => $ascIdx,
                'moon_sign'      => $pos['moon_sign'] ?? null,
                'moon_nakshatra' => $pos['moon_nakshatra'] ?? null,
                'planets'        => $planets,
            ];
        } catch (Throwable) {
            return ['available' => false, 'planets' => []];
        }
    }

    private static function planetAbbr(string $name): string
    {
        return [
            'Sun' => 'Su', 'Moon' => 'Mo', 'Mars' => 'Ma', 'Mercury' => 'Me',
            'Jupiter' => 'Ju', 'Venus' => 'Ve', 'Saturn' => 'Sa',
            'Rahu' => 'Ra', 'Ketu' => 'Ke',
        ][$name] ?? substr($name, 0, 2);
    }

    /** Location + timezone from site_settings (falls back to config/panchang defaults). */
    private function geo(): array
    {
        return [
            (float) Setting::get('latitude', config('panchang.lat', 21.1702)),
            (float) Setting::get('longitude', config('panchang.lon', 72.8311)),
            (string) Setting::get('timezone', 'Asia/Kolkata'),
        ];
    }

    /**
     * Full live panchang for a date — verbatim port of the PHP `calc_full_panchang`
     * (tithi/nakshatra/yoga/karana/masa/paksha/samvatsara + sun/moon + rahu/abhijit/
     * gulika/yamaganda/dur-muhurta/varjyam/amrit kaal). hindu_calendar overrides win.
     */
    private function panchang(?string $date = null, ?float $ovLat = null, ?float $ovLon = null): array
    {
        [$gLat, $gLon, $tz] = $this->geo();
        $lat = $ovLat ?? $gLat;
        $lon = $ovLon ?? $gLon;
        $date = $date ?: date('Y-m-d');

        $p = [];
        try {
            self::loadEngine();
            $p = calc_full_panchang($date, $lat, $lon, $tz);
        } catch (Throwable) {
        }
        // Admin-entered calendar values (hindu_calendar) take priority when present.
        try {
            $row = DB::table('hindu_calendar')->where('cal_date', $date)->first();
            if ($row) {
                foreach (['tithi', 'nakshatra', 'yoga', 'karana', 'masa', 'paksha', 'samvatsara', 'tithi_dev', 'nakshatra_dev', 'festival'] as $k) {
                    if (isset($row->$k) && $row->$k !== null && $row->$k !== '') {
                        $p[$k] = $row->$k;
                    }
                }
            }
        } catch (Throwable) {
        }
        $p['date_label'] = date('l, d M Y', strtotime($date));
        return $p;
    }

    /**
     * GET /api/public/panchang?date=YYYY-MM-DD — full detail-page payload:
     * full panchang + realtime choghadiya (day/night) + 3-day outlook + day specials.
     * Port of the PHP public `panchang` route.
     */
    public function panchangDetail(Request $request): JsonResponse
    {
        $today = date('Y-m-d');
        $date = (string) $request->query('date', $today);
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $date = $today;
        }
        // Optional location override (e.g. the user's current city) for a
        // location-accurate panchang; falls back to the configured geo.
        $lat = is_numeric($request->query('lat')) ? (float) $request->query('lat') : null;
        $lon = is_numeric($request->query('lon')) ? (float) $request->query('lon') : null;
        return $this->ok($this->panchangArray($date, $lat, $lon));
    }

    /**
     * Full panchang payload for a date (panch + choghadiya + week + specials +
     * kundali). Reusable by the JSON endpoint, the share page and the PDF.
     */
    public function panchangArray(string $date, ?float $ovLat = null, ?float $ovLon = null): array
    {
        [$gLat, $gLon, $tz] = $this->geo();
        $lat = $ovLat ?? $gLat;
        $lon = $ovLon ?? $gLon;
        $today = date('Y-m-d');
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            $date = $today;
        }

        $panch = $this->panchang($date, $ovLat, $ovLon);
        $chog = ['day' => [], 'night' => []];
        $week = [];
        try {
            self::loadEngine();
            $wd = (int) date('w', strtotime($date));
            $raw = calc_choghadiya_realtime($wd, $panch['sunrise'] ?? '06:00', $panch['sunset'] ?? '18:00', $date, $tz);
            foreach ($raw as $c) {
                if (!is_array($c)) {
                    continue;
                }
                $slot = [
                    'name'    => $c['name'] ?? '',
                    'start'   => $c['start24'] ?? ($c['start'] ?? ''),
                    'end'     => $c['end24'] ?? ($c['end'] ?? ''),
                    'quality' => $c['quality'] ?? '',
                    'good'    => in_array($c['quality'] ?? '', ['auspicious', 'good'], true),
                    'active'  => (bool) ($c['active'] ?? false),
                ];
                if (($c['type'] ?? 'day') === 'night') {
                    $chog['night'][] = $slot;
                } else {
                    $chog['day'][] = $slot;
                }
            }
            $week = calc_week_panchang(3, $lat, $lon, $tz, 0);
        } catch (Throwable) {
        }

        $daySpecials = $this->matchDaySpecials($date, $panch, false);

        // Also fold in the Festivals master. English festivals recur on the same
        // month + day every year (year ignored); Hindu festivals match masa /
        // paksha / tithi. Normalised to the day-special shape and de-duped by name.
        try {
            $seen = [];
            foreach ($daySpecials as $ds) {
                $seen[strtolower(trim((string) ($ds['name'] ?? '')))] = true;
            }
            foreach (\App\Support\FestivalMatcher::forDate($date, $panch) as $f) {
                $key = strtolower(trim((string) ($f['name'] ?? '')));
                if ($key === '' || isset($seen[$key])) {
                    continue;
                }
                $seen[$key] = true;
                $daySpecials[] = [
                    'name'        => $f['name'] ?? '',
                    'name_dev'    => $f['name_dev'] ?? '',
                    'type'        => 'Festival',
                    'description' => trim(strip_tags((string) ($f['significance'] ?? ''))),
                    'image_url'   => $f['image_url'] ?? '',
                    'match_type'  => $f['match_type'] ?? '',
                    'masa'        => $f['masa'] ?? '',
                    'paksha'      => $f['paksha'] ?? '',
                    'tithi'       => $f['tithi'] ?? '',
                    'deity'       => $f['deity'] ?? '',
                ];
            }
        } catch (Throwable) {
        }

        return [
            'panch'        => $panch,
            'choghadiya'   => $chog,
            'week'         => $week,
            'day_specials' => $daySpecials,
            'kundali'      => $this->todayKundali($date, $lat, $lon, $tz),
            'req_date'     => $date,
            'today'        => $today,
            'lat'          => $lat,
            'lon'          => $lon,
            'tz'           => $tz,
        ];
    }

    /** Services grouped by category (as the PHP landing renders them). */
    private function servicesByCat(): array
    {
        $out = [];
        foreach ($this->services() as $s) {
            $cat = (string) ($s['category'] ?? 'other') ?: 'other';
            $out[$cat][] = $s;
        }
        return $out;
    }

    /**
     * Featured poojas — the Main Services (booking_services) explicitly ticked
     * "Show in Featured Poojas" in the Booking Catalogue. Simple on/off flag
     * (is_featured); no heuristics. Includes deity name + slug for the cards.
     */
    private function poojas(): array
    {
        \App\Support\BookingCatalog::ensure();
        $table = 'booking_services';
        try {
            if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'is_featured')) {
                return [];
            }
            $q = DB::table($table . ' as p')
                ->where('p.is_active', 1)
                ->where('p.is_featured', 1);
            $cols = ['p.id', 'p.name', 'p.name_dev', 'p.description', 'p.duration_min'];
            if (Schema::hasColumn($table, 'slug')) { $cols[] = 'p.slug'; }
            if (Schema::hasColumn($table, 'category')) { $cols[] = 'p.category'; }
            if (Schema::hasColumn($table, 'deity_id') && Schema::hasTable('deities')) {
                $q->leftJoin('deities as d', 'd.id', '=', 'p.deity_id');
                $cols[] = 'd.name as deity_name';
            }
            $rows = $q->orderByRaw('COALESCE(p.landing_order, p.sort_order, 0)')->orderBy('p.name')->limit(12)->get($cols);
            return $rows->map(fn ($r) => (array) $r)->all();
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * Day specials + festivals that actually fall on TODAY, for the landing
     * panchang widget. Only rows whose date rules (English month/day or Hindu
     * masa/paksha/tithi) match today are returned — a dated special such as
     * Republic Day (26 Jan) no longer shows on every other day. Landing-gated
     * (show_on_landing), then today's matching festivals from the Festivals
     * master are folded in (they are date-matched by definition).
     */
    private function daySpecials(): array
    {
        try {
            $date  = date('Y-m-d');
            $panch = $this->panchang($date);

            $rows = $this->matchDaySpecials($date, $panch, true);

            $seen = [];
            foreach ($rows as $r) {
                $seen[strtolower(trim((string) ($r['name'] ?? '')))] = true;
            }
            try {
                foreach (\App\Support\FestivalMatcher::forDate($date, $panch) as $f) {
                    $key = strtolower(trim((string) ($f['name'] ?? '')));
                    if ($key === '' || isset($seen[$key])) {
                        continue;
                    }
                    $seen[$key] = true;
                    $rows[] = [
                        'name'        => $f['name'] ?? '',
                        'name_dev'    => $f['name_dev'] ?? '',
                        'type'        => 'Festival',
                        'description' => trim(strip_tags((string) ($f['significance'] ?? ''))),
                    ];
                }
            } catch (Throwable) {
            }

            // Return only the fields the widget renders, capped.
            $rows = array_map(fn ($r) => [
                'name'        => (string) ($r['name'] ?? ''),
                'name_dev'    => (string) ($r['name_dev'] ?? ''),
                'type'        => (string) ($r['type'] ?? ''),
                'description' => (string) ($r['description'] ?? ''),
            ], $rows);
            return array_slice($rows, 0, 6);
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * day_specials rows whose date rules match the given date. `$landingOnly`
     * additionally restricts to rows flagged for the landing widget. Shared by
     * the landing widget and the full panchang payload so both apply the exact
     * same date matching (English month/day/weekday, or Hindu masa/paksha/tithi
     * with blank fields treated as wildcards).
     */
    private function matchDaySpecials(string $date, array $panch, bool $landingOnly = false): array
    {
        try {
            $m  = (int) date('n', strtotime($date));
            $dd = (int) date('j', strtotime($date));
            $wd = (int) date('w', strtotime($date));
            // Hindu-calendar values for the day (lower-cased for tolerant matching).
            $masa   = strtolower(trim((string) ($panch['masa'] ?? '')));
            $paksha = strtolower(trim((string) ($panch['paksha'] ?? '')));
            $tithi  = strtolower(trim((string) ($panch['tithi'] ?? '')));

            $q = DB::table('day_specials')->where('is_active', 1)
                ->where(function ($outer) use ($m, $dd, $wd, $masa, $paksha, $tithi) {
                    // (A) English-calendar match: month + day (+ optional weekday).
                    $outer->where(function ($q2) use ($m, $dd, $wd) {
                        $q2->whereIn('match_type', ['english_date', 'both'])
                            ->where(fn ($x) => $x->whereNull('eng_month')->orWhere('eng_month', '')->orWhere('eng_month', $m))
                            ->where(fn ($x) => $x->whereNull('eng_day')->orWhere('eng_day', '')->orWhere('eng_day', $dd))
                            ->where(fn ($x) => $x->whereNull('weekday')->orWhere('weekday', '')->orWhere('weekday', $wd));
                    });
                    // (B) Hindu-calendar match: masa / paksha / tithi. Any field the
                    //     row specifies must match the day; blank fields are wildcards.
                    //     At least one Hindu field must be set (so all-blank rows don't
                    //     match every day). Tolerant "contains" comparison, case-insensitive.
                    $outer->orWhere(function ($q3) use ($masa, $paksha, $tithi) {
                        $q3->whereIn('match_type', ['hindu_date', 'both'])
                            ->where(function ($h) {
                                $h->where(fn ($x) => $x->whereNotNull('tithi')->where('tithi', '!=', ''))
                                    ->orWhere(fn ($x) => $x->whereNotNull('masa')->where('masa', '!=', ''))
                                    ->orWhere(fn ($x) => $x->whereNotNull('paksha')->where('paksha', '!=', ''));
                            })
                            ->where(fn ($x) => $x->whereNull('masa')->orWhere('masa', '')->orWhereRaw('? LIKE CONCAT(\'%\', LOWER(masa), \'%\')', [$masa]))
                            ->where(fn ($x) => $x->whereNull('paksha')->orWhere('paksha', '')->orWhereRaw('? LIKE CONCAT(\'%\', LOWER(paksha), \'%\')', [$paksha]))
                            ->where(fn ($x) => $x->whereNull('tithi')->orWhere('tithi', '')->orWhereRaw('? LIKE CONCAT(\'%\', LOWER(tithi), \'%\')', [$tithi]));
                    });
                });
            if ($landingOnly && Schema::hasColumn('day_specials', 'show_on_landing')) {
                $q->where('show_on_landing', 1);
            }
            if (Schema::hasColumn('day_specials', 'landing_order')) {
                $q->orderBy('landing_order');
            }
            return $q->orderBy('id')->limit(20)->get()->map(fn ($r) => (array) $r)->all();
        } catch (Throwable) {
            return [];
        }
    }

    /** Numerology feature cards parsed from the num_features setting JSON. */
    private function numFeatures(): array
    {
        $default = [
            ['icon' => '👤', 'title' => 'Mulank & Bhagyank', 'desc' => 'Core life numbers from your date of birth'],
            ['icon' => '🌟', 'title' => 'Namank Analysis', 'desc' => 'Chaldean name numerology breakdown'],
            ['icon' => '🔳', 'title' => 'Lo Shu Grid', 'desc' => '9-cell grid analysis of your DOB digits'],
            ['icon' => '🪐', 'title' => 'Planetary Dasha', 'desc' => 'Current planetary period and predictions'],
            ['icon' => '📅', 'title' => 'Personal Year/Month', 'desc' => 'Your current life cycle themes'],
            ['icon' => '💎', 'title' => 'Gemstone Remedies', 'desc' => 'Lucky stones based on your numbers'],
            ['icon' => '🏠', 'title' => 'House Number', 'desc' => 'Home energy analysis'],
            ['icon' => '📱', 'title' => 'Mobile Number', 'desc' => 'Lucky number check and suggestions'],
        ];
        $raw = (string) Setting::get('num_features', '');
        if ($raw !== '') {
            $parsed = json_decode($raw, true);
            if (is_array($parsed) && $parsed) {
                return $parsed;
            }
        }
        return $default;
    }

    /** GET /api/public/services — landing services grid. */
    public function servicesList(): JsonResponse
    {
        return $this->ok(['services' => $this->services()]);
    }

    /**
     * GET /api/public/poojas — every active pooja / main service that HAS a
     * Pooja Type set (any value). Services without a pooja type are excluded.
     * Powers the "View all" listing from the home Featured Poojas section.
     */
    public function poojasAll(): JsonResponse
    {
        \App\Support\BookingCatalog::ensure();
        $table = 'booking_services';
        try {
            if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'pooja_type')) {
                return $this->ok(['services' => []]);
            }
            $q = DB::table($table . ' as p')
                ->where('p.is_active', 1)
                ->whereNotNull('p.pooja_type')
                ->where('p.pooja_type', '!=', '');
            $cols = ['p.id', 'p.name', 'p.name_dev', 'p.description', 'p.duration_min', 'p.pooja_type'];
            if (Schema::hasColumn($table, 'slug')) { $cols[] = 'p.slug'; }
            if (Schema::hasColumn($table, 'category')) { $cols[] = 'p.category'; }
            if (Schema::hasColumn($table, 'icon')) { $cols[] = 'p.icon'; }
            if (Schema::hasColumn($table, 'tag_line')) { $cols[] = 'p.tag_line'; }
            if (Schema::hasColumn($table, 'is_online')) { $cols[] = 'p.is_online'; }
            if (Schema::hasColumn($table, 'is_offline')) { $cols[] = 'p.is_offline'; }
            if (Schema::hasColumn($table, 'base_fee')) { $cols[] = 'p.base_fee'; }
            if (Schema::hasColumn($table, 'deity_id') && Schema::hasTable('deities')) {
                $q->leftJoin('deities as d', 'd.id', '=', 'p.deity_id');
                $cols[] = 'd.name as deity_name';
            }
            $rows = $q->orderByRaw('COALESCE(p.landing_order, p.sort_order, 0)')->orderBy('p.name')->limit(200)->get($cols);
            return $this->ok(['services' => $rows->map(fn ($r) => (array) $r)->all()]);
        } catch (Throwable) {
            return $this->ok(['services' => []]);
        }
    }

    /**
     * GET /api/public/services/by-category/{cat} — every active service in a
     * category (not gated on show_on_landing), for the "View all" listing page.
     */
    public function servicesByCategory(string $cat): JsonResponse
    {
        $cat = trim(urldecode($cat));
        return $this->ok([
            'category' => $cat,
            'services' => $cat === '' ? [] : $this->servicesForCategory($cat),
        ]);
    }

    /**
     * GET /api/public/service/{key} — full detail for one service / pooja
     * (booking_services), resolved by numeric id or slug. Returns the service,
     * its peripheral poojas (add-ons), and the mapped material list (from
     * pooja_mat_mapping) split into what the Yajman brings vs the Pandit brings,
     * for the main pooja and for each peripheral. Powers the public detail page's
     * Description / Materials / Add-ons tabs.
     */
    public function serviceDetail(string $key): JsonResponse
    {
        \App\Support\BookingCatalog::ensure();
        $table = 'booking_services';
        try {
            if (!Schema::hasTable($table)) {
                return $this->fail('Service not found', 404);
            }
            $want = ['id', 'slug', 'name', 'name_dev', 'icon', 'category', 'tag_line', 'description',
                'highlights', 'base_fee', 'fee_type', 'badge', 'duration_min', 'is_online', 'is_offline', 'input_fields',
                'deity_id', 'pooja_type', 'linked_services'];
            $cols = array_values(array_filter($want, fn ($c) => Schema::hasColumn($table, $c)));

            $q = DB::table($table)->where('is_active', 1);
            if (ctype_digit($key)) {
                $q->where('id', (int) $key);
            } elseif (Schema::hasColumn($table, 'slug')) {
                $q->where('slug', $key);
            } else {
                $q->where('name', $key);
            }
            $row = $q->first($cols);
            if (!$row) {
                return $this->fail('Service not found', 404);
            }
            $s = (array) $row;
            if (isset($s['input_fields'])) {
                $decoded = !empty($s['input_fields']) ? json_decode((string) $s['input_fields'], true) : [];
                $s['input_fields'] = is_array($decoded) ? $decoded : [];
            }
            $s['price'] = $s['base_fee'] ?? null;
            $s['price_type'] = $s['fee_type'] ?? null;
            $sid = (int) ($s['id'] ?? 0);

            // Deity name for display.
            $s['deity_name'] = null;
            try {
                if (!empty($s['deity_id']) && Schema::hasTable('deities')) {
                    $s['deity_name'] = (string) (DB::table('deities')->where('id', (int) $s['deity_id'])->value('name') ?? '') ?: null;
                }
            } catch (Throwable) {
            }

            // Linked poojas — other main services suggested to "club" at booking.
            $linked = [];
            try {
                $ids = [];
                if (!empty($s['linked_services'])) {
                    $decoded = json_decode((string) $s['linked_services'], true);
                    if (is_array($decoded)) {
                        $ids = array_values(array_filter(array_map('intval', $decoded), fn ($i) => $i > 0 && $i !== $sid));
                    }
                }
                if ($ids) {
                    $lcols = array_values(array_filter(['id', 'slug', 'name', 'name_dev', 'icon', 'duration_min', 'base_fee', 'fee_type'],
                        fn ($c) => Schema::hasColumn($table, $c)));
                    $rows = DB::table($table)->whereIn('id', $ids)->where('is_active', 1)->get($lcols);
                    // preserve the admin-chosen order
                    $byId = [];
                    foreach ($rows as $r) {
                        $byId[(int) $r->id] = (array) $r;
                    }
                    foreach ($ids as $i) {
                        if (isset($byId[$i])) {
                            $linked[] = $byId[$i];
                        }
                    }
                }
            } catch (Throwable) {
            }
            unset($s['linked_services']);

            // Peripheral poojas (add-ons) of this main pooja.
            $peripherals = [];
            try {
                foreach (\App\Support\BookingCatalog::addonsForService($sid) as $a) {
                    $peripherals[] = [
                        'id'       => (int) ($a['id'] ?? 0),
                        'name'     => (string) ($a['name'] ?? ''),
                        'name_dev' => (string) ($a['name_dev'] ?? ''),
                    ];
                }
            } catch (Throwable) {
            }

            // Mapped materials (pooja_mat_mapping) for this service, grouped by add-on.
            $materials = ['main' => ['yajman' => [], 'pandit' => []], 'by_addon' => []];
            try {
                if (Schema::hasTable('pooja_mat_mapping') && $sid > 0) {
                    $rows = DB::table('pooja_mat_mapping')
                        ->where('service_id', $sid)
                        ->orderBy('addon_id')->orderBy('id')
                        ->get(['addon_id', 'addon_name', 'product_sku', 'product_name', 'brought_by', 'qty']);
                    $addonGroups = [];
                    foreach ($rows as $r) {
                        $line = [
                            'sku'        => (string) ($r->product_sku ?? ''),
                            'name'       => (string) ($r->product_name ?? ''),
                            'brought_by' => ((string) ($r->brought_by ?? 'yajman')) === 'pandit' ? 'pandit' : 'yajman',
                            'qty'        => (float) ($r->qty ?? 1),
                        ];
                        $aid = (int) ($r->addon_id ?? 0);
                        if ($aid === 0) {
                            $materials['main'][$line['brought_by']][] = $line;
                        } else {
                            if (!isset($addonGroups[$aid])) {
                                $addonGroups[$aid] = [
                                    'addon_id'   => $aid,
                                    'addon_name' => (string) ($r->addon_name ?? ''),
                                    'yajman'     => [],
                                    'pandit'     => [],
                                ];
                            }
                            $addonGroups[$aid][$line['brought_by']][] = $line;
                        }
                    }
                    $materials['by_addon'] = array_values($addonGroups);
                }
            } catch (Throwable) {
            }
            $materials['count'] = count($materials['main']['yajman']) + count($materials['main']['pandit'])
                + array_sum(array_map(fn ($g) => count($g['yajman']) + count($g['pandit']), $materials['by_addon']));

            // Ritual steps assigned to this pooja (its Pooja Sequence) — for the
            // "Pooja Rituals" tab. Each step resolves its referenced chant for the
            // Sanskrit text + meaning.
            $steps = [];
            try {
                $slug = (string) ($s['slug'] ?? '');
                if ($slug !== '' && Schema::hasTable('pooja_steps')) {
                    $q = DB::table('pooja_steps')->where('pooja_code', $slug);
                    if (Schema::hasColumn('pooja_steps', 'deleted_at')) {
                        $q->whereNull('deleted_at');
                    }
                    if (Schema::hasColumn('pooja_steps', 'is_active')) {
                        $q->where('is_active', 1);
                    }
                    $rows = $q->orderByRaw('COALESCE(step_no, 999999)')->orderBy('id')->get();

                    // Resolve referenced chants (mantra_ref stores chants.name).
                    $names = [];
                    foreach ($rows as $r) {
                        $mn = trim((string) ($r->mantra_ref ?? ''));
                        if ($mn !== '') {
                            $names[$mn] = true;
                        }
                    }
                    $chantMap = [];
                    if ($names && Schema::hasTable('chants')) {
                        $ccols = array_values(array_filter(
                            ['name', 'name_dev', 'sanskrit_text', 'meaning', 'purpose'],
                            fn ($c) => Schema::hasColumn('chants', $c),
                        ));
                        foreach (DB::table('chants')->whereIn('name', array_keys($names))->get($ccols) as $c) {
                            $chantMap[(string) $c->name] = (array) $c;
                        }
                    }

                    foreach ($rows as $r) {
                        $rr = (array) $r;
                        $mn = trim((string) ($rr['mantra_ref'] ?? ''));
                        $chant = ($mn !== '' && isset($chantMap[$mn])) ? $chantMap[$mn] : null;
                        $mats = array_values(array_filter(array_map('trim', explode(',', (string) ($rr['materials'] ?? '')))));
                        $steps[] = [
                            'step_no'          => isset($rr['step_no']) && $rr['step_no'] !== null ? (int) $rr['step_no'] : null,
                            'title'            => (string) ($rr['title'] ?? ''),
                            'title_dev'        => (string) ($rr['title_dev'] ?? ''),
                            'phase'            => (string) ($rr['phase'] ?? ''),
                            'is_optional'      => (int) ($rr['is_optional'] ?? 0),
                            'who_recites'      => (string) ($rr['who_recites'] ?? ''),
                            'recitation_style' => (string) ($rr['recitation_style'] ?? ''),
                            'kriya'            => (string) ($rr['kriya_action'] ?? ''),
                            'mudra'            => (string) ($rr['mudra_posture'] ?? ''),
                            'duration_min'     => isset($rr['duration_min']) && $rr['duration_min'] !== null ? (int) $rr['duration_min'] : null,
                            'mantra_ref'       => $mn,
                            'materials'        => $mats,
                            'chant'            => $chant ? [
                                'name'     => (string) ($chant['name'] ?? ''),
                                'name_dev' => (string) ($chant['name_dev'] ?? ''),
                                'sanskrit' => (string) ($chant['sanskrit_text'] ?? ''),
                                'meaning'  => (string) ($chant['meaning'] ?? ($chant['purpose'] ?? '')),
                            ] : null,
                        ];
                    }
                }
            } catch (Throwable) {
            }

            return $this->ok([
                'service'     => $s,
                'peripherals' => $peripherals,
                'linked'      => $linked,
                'materials'   => $materials,
                'steps'       => $steps,
            ]);
        } catch (Throwable) {
            return $this->fail('Service not found', 404);
        }
    }

    /**
     * GET /api/public/pooja/{key}/booklet.pdf — a printable Pooja Booklet:
     * VPandit cover → About → step-by-step ritual vidhi → materials list →
     * closing VPandit brand page. Rendered with mPDF (full Devanagari).
     */
    public function poojaBooklet(string $key): \Illuminate\Http\Response
    {
        \App\Support\BookingCatalog::ensure();
        $table = 'booking_services';
        $want = ['id', 'slug', 'name', 'name_dev', 'icon', 'category', 'tag_line', 'description',
            'highlights', 'base_fee', 'fee_type', 'duration_min', 'deity_id'];
        $cols = array_values(array_filter($want, fn ($c) => Schema::hasColumn($table, $c)));
        $q = DB::table($table)->where('is_active', 1);
        if (ctype_digit($key)) {
            $q->where('id', (int) $key);
        } elseif (Schema::hasColumn($table, 'slug')) {
            $q->where('slug', $key);
        } else {
            $q->where('name', $key);
        }
        $row = $q->first($cols);
        if (!$row) {
            abort(404, 'Service not found');
        }
        $s = (array) $row;
        $sid = (int) ($s['id'] ?? 0);
        $slug = (string) ($s['slug'] ?? '');

        $deity = '';
        try {
            if (!empty($s['deity_id']) && Schema::hasTable('deities')) {
                $deity = (string) (DB::table('deities')->where('id', (int) $s['deity_id'])->value('name') ?? '');
            }
        } catch (Throwable) {
        }

        // Steps + referenced chants.
        $steps = [];
        try {
            if ($slug !== '' && Schema::hasTable('pooja_steps')) {
                $q2 = DB::table('pooja_steps')->where('pooja_code', $slug);
                if (Schema::hasColumn('pooja_steps', 'deleted_at')) {
                    $q2->whereNull('deleted_at');
                }
                if (Schema::hasColumn('pooja_steps', 'is_active')) {
                    $q2->where('is_active', 1);
                }
                $rows = $q2->orderByRaw('COALESCE(step_no, 999999)')->orderBy('id')->get();
                $names = [];
                foreach ($rows as $r) {
                    $m = trim((string) ($r->mantra_ref ?? ''));
                    if ($m !== '') {
                        $names[$m] = true;
                    }
                }
                $cmap = [];
                if ($names && Schema::hasTable('chants')) {
                    $cc = array_values(array_filter(['name', 'name_dev', 'sanskrit_text', 'meaning', 'purpose'],
                        fn ($c) => Schema::hasColumn('chants', $c)));
                    foreach (DB::table('chants')->whereIn('name', array_keys($names))->get($cc) as $c) {
                        $cmap[(string) $c->name] = (array) $c;
                    }
                }
                foreach ($rows as $r) {
                    $rr = (array) $r;
                    $m = trim((string) ($rr['mantra_ref'] ?? ''));
                    $ch = ($m !== '' && isset($cmap[$m])) ? $cmap[$m] : null;
                    $steps[] = [
                        'step_no' => $rr['step_no'] ?? null,
                        'title' => (string) ($rr['title'] ?? ''),
                        'title_dev' => (string) ($rr['title_dev'] ?? ''),
                        'phase' => (string) ($rr['phase'] ?? ''),
                        'is_optional' => (int) ($rr['is_optional'] ?? 0),
                        'who' => (string) ($rr['who_recites'] ?? ''),
                        'style' => (string) ($rr['recitation_style'] ?? ''),
                        'kriya' => (string) ($rr['kriya_action'] ?? ''),
                        'mudra' => (string) ($rr['mudra_posture'] ?? ''),
                        'dur' => $rr['duration_min'] ?? null,
                        'mats' => array_values(array_filter(array_map('trim', explode(',', (string) ($rr['materials'] ?? ''))))),
                        'chant' => $ch ? [
                            'dev' => (string) ($ch['sanskrit_text'] ?? ($ch['name_dev'] ?? '')),
                            'mean' => (string) ($ch['meaning'] ?? ($ch['purpose'] ?? '')),
                        ] : null,
                    ];
                }
            }
        } catch (Throwable) {
        }

        // Materials (distinct by name).
        $mat = [];
        try {
            if ($sid > 0 && Schema::hasTable('pooja_mat_mapping')) {
                $seen = [];
                foreach (DB::table('pooja_mat_mapping')->where('service_id', $sid)
                    ->orderBy('addon_id')->orderBy('id')->get(['product_name', 'qty', 'brought_by']) as $r) {
                    $n = trim((string) $r->product_name);
                    if ($n === '' || isset($seen[mb_strtolower($n)])) {
                        continue;
                    }
                    $seen[mb_strtolower($n)] = true;
                    $mat[] = [
                        'name' => $n,
                        'qty' => (float) $r->qty,
                        'by' => ((string) $r->brought_by) === 'pandit' ? 'Pandit' : 'Yajman',
                    ];
                }
            }
        } catch (Throwable) {
        }

        $html = $this->bookletHtml($s, $deity, $steps, $mat);
        $file = 'pooja-' . (preg_replace('/[^A-Za-z0-9]+/', '-', $slug ?: (string) $sid)) . '-booklet.pdf';
        return (new \App\Support\PdfService())->download($html, $file, ((string) ($s['name'] ?? 'Pooja')) . ' — Booklet', false);
    }

    /** Build the Pooja Booklet HTML (mPDF-friendly). */
    private function bookletHtml(array $s, string $deity, array $steps, array $mat): string
    {
        $who = ['PANDIT' => 'Pandit only', 'YAJAMANA' => 'Yajaman only', 'BOTH' => 'Both in unison', 'PANDIT_LEADS' => 'Pandit leads · Yajaman repeats'];
        $sty = ['UCHCHA' => 'Uchcha (aloud)', 'UPANSHU' => 'Upanshu (whisper)', 'MANASIKA' => 'Manasika (mental)'];
        $phL = ['PURVANGA' => 'Purvanga', 'MUKHYA' => 'Mukhya', 'UTTARANGA' => 'Uttaranga'];

        $name = e((string) ($s['name'] ?? 'Pooja'));
        $nameDev = e((string) ($s['name_dev'] ?? ''));
        $cat = e((string) ($s['category'] ?? ''));
        $deityE = e($deity);
        $dur = !empty($s['duration_min']) ? ((int) $s['duration_min']) . ' min' : '';
        $fee = (!empty($s['base_fee']) && (float) $s['base_fee'] > 0) ? '₹' . number_format((float) $s['base_fee']) : '';
        $desc = trim((string) ($s['description'] ?? ''));
        $today = date('d M Y');

        // ---- Cover ----
        $cover = '<div class="cover">'
            . '<div class="om">ॐ</div>'
            . '<div class="brand">V&nbsp;P&nbsp;A&nbsp;N&nbsp;D&nbsp;I&nbsp;T</div>'
            . '<div class="brand-sub">Vedic Rituals · Pooja · Astrology</div>'
            . '<div class="rule"></div>'
            . '<div class="cover-kicker">Pooja Booklet — Ritual Guide</div>'
            . ($nameDev !== '' ? '<div class="cover-dev">' . $nameDev . '</div>' : '')
            . '<div class="cover-name">' . $name . '</div>'
            . '<div class="cover-meta">' . trim(implode(' · ', array_filter([
                $deityE !== '' ? 'Deity: ' . $deityE : '', $cat, $dur ? 'Duration: ' . $dur : '',
            ]))) . '</div>'
            . '</div>';

        // ---- About ----
        $hl = '';
        foreach (preg_split('/\r\n|\r|\n/', (string) ($s['highlights'] ?? '')) as $ln) {
            $ln = trim((string) $ln);
            if ($ln !== '') {
                $hl .= '<li>' . e($ln) . '</li>';
            }
        }
        $about = '<div class="page"><h2>About this Pooja</h2>'
            . ($desc !== '' ? '<p class="lead">' . nl2br(e($desc)) . '</p>' : '<p class="muted">—</p>')
            . ($hl !== '' ? '<h3>Highlights</h3><ul>' . $hl . '</ul>' : '')
            . '<table class="kv"><tbody>'
            . ($deityE !== '' ? '<tr><td>Deity</td><td>' . $deityE . '</td></tr>' : '')
            . ($cat !== '' ? '<tr><td>Category</td><td>' . $cat . '</td></tr>' : '')
            . ($dur !== '' ? '<tr><td>Duration</td><td>' . e($dur) . '</td></tr>' : '')
            . '<tr><td>Ritual steps</td><td>' . count($steps) . '</td></tr>'
            . '<tr><td>Samagri items</td><td>' . count($mat) . '</td></tr>'
            . ($fee !== '' ? '<tr><td>Dakshina / Fee</td><td>' . e($fee) . '</td></tr>' : '')
            . '</tbody></table></div>';

        // ---- Steps ----
        $stepHtml = '';
        foreach ($steps as $i => $st) {
            $no = $st['step_no'] !== null ? (int) $st['step_no'] : ($i + 1);
            $tag = $st['is_optional'] ? '<span class="tg opt">Optional</span>' : '<span class="tg req">Compulsory</span>';
            $ph = $st['phase'] !== '' ? '<span class="tg ph">' . e($phL[$st['phase']] ?? $st['phase']) . '</span>' : '';
            $du = $st['dur'] ? '<span class="tg dur">' . (int) $st['dur'] . ' min</span>' : '';
            $chant = '';
            if ($st['chant'] && ($st['chant']['dev'] !== '' || $st['chant']['mean'] !== '')) {
                $chant = '<div class="chant">'
                    . ($st['chant']['dev'] !== '' ? '<div class="c-dev">' . $st['chant']['dev'] . '</div>' : '')
                    . ($st['chant']['mean'] !== '' ? '<div class="c-mean"><b>Meaning.</b> ' . e($st['chant']['mean']) . '</div>' : '')
                    . '</div>';
            }
            $metaBits = [];
            if ($st['who'] !== '') {
                $metaBits[] = '<b>Recites:</b> ' . e($who[$st['who']] ?? $st['who']);
            }
            if ($st['style'] !== '') {
                $metaBits[] = '<b>Style:</b> ' . e($sty[$st['style']] ?? $st['style']);
            }
            if ($st['mudra'] !== '') {
                $metaBits[] = '<b>Mudra:</b> ' . e($st['mudra']);
            }
            $meta = $metaBits ? '<div class="s-meta">' . implode(' &nbsp;·&nbsp; ', $metaBits) . '</div>' : '';
            $kriya = $st['kriya'] !== '' ? '<div class="s-kriya"><b>Kriyā.</b> ' . e($st['kriya']) . '</div>' : '';
            $mats = '';
            foreach ($st['mats'] as $mm) {
                $mats .= '<span class="chip">' . e($mm) . '</span> ';
            }
            $mats = $mats !== '' ? '<div class="s-mats">' . $mats . '</div>' : '';
            $stepHtml .= '<div class="step"><table class="s-head"><tr>'
                . '<td class="s-no">' . $no . '</td>'
                . '<td class="s-ttl">' . ($st['title_dev'] !== '' ? '<span class="s-dev">' . e($st['title_dev']) . '</span> ' : '') . '<span class="s-en">' . e($st['title']) . '</span></td>'
                . '<td class="s-tags">' . $ph . $tag . $du . '</td>'
                . '</tr></table>' . $chant . $meta . $kriya . $mats . '</div>';
        }
        $stepsSection = '<div class="page"><h2>Ritual Steps — Pooja Vidhi</h2>'
            . '<p class="muted small">Phase: Purvanga (preparation) · Mukhya (main) · Uttaranga (conclusion)</p>'
            . ($stepHtml !== '' ? $stepHtml : '<p class="muted">No steps have been assigned to this pooja yet.</p>')
            . '</div>';

        // ---- Materials ----
        $matRows = '';
        foreach ($mat as $i => $m) {
            $qty = rtrim(rtrim(number_format($m['qty'], 2), '0'), '.');
            $matRows .= '<tr><td>' . ($i + 1) . '</td><td>' . e($m['name']) . '</td><td>' . e($qty ?: '1') . '</td><td>' . e($m['by']) . '</td></tr>';
        }
        $matsSection = '<div class="page"><h2>Samagri — Materials Required</h2>'
            . ($matRows !== ''
                ? '<table class="mat"><thead><tr><th style="width:8%">#</th><th>Material</th><th style="width:18%">Qty</th><th style="width:22%">Brought by</th></tr></thead><tbody>' . $matRows . '</tbody></table>'
                : '<p class="muted">The material list will be shared by the assigned pandit at booking.</p>')
            . '</div>';

        // ---- Closing ----
        $closing = '<div class="closing">'
            . '<div class="om2">ॐ</div>'
            . '<div class="bless">॥ शुभं भवतु ॥</div>'
            . '<div class="bless-en">May this worship bring peace, prosperity and blessings.</div>'
            . '<div class="rule2"></div>'
            . '<div class="brand2">V P A N D I T</div>'
            . '<div class="brand-sub2">Vedic Rituals · Pooja · Jyotish · Vastu · Store</div>'
            . '<div class="foot2">This booklet was generated by VPandit · ' . e($today) . '</div>'
            . '</div>';

        return '<html><head><style>'
            // Base font = DejaVu (full Latin). mPDF's autoScriptToLang switches
            // Devanagari runs to the notodeva font automatically, so English and
            // Hindi both render (the notodeva TTF has no Latin glyphs).
            . 'body{font-family:dejavusans,sans-serif;color:#241a12;font-size:11pt;line-height:1.55}'
            . 'h2{font-size:15pt;color:#7A1414;border-bottom:2px solid #E7CE9E;padding-bottom:4px;margin:0 0 10px}'
            . 'h3{font-size:12pt;color:#8a5a00;margin:14px 0 4px}'
            . '.lead{font-size:11.5pt;color:#33261a}'
            . '.muted{color:#8a7a68}.small{font-size:9.5pt}'
            . 'ul{margin:4px 0 8px 0;padding-left:18px}li{margin:2px 0}'
            . 'table.kv{width:100%;border-collapse:collapse;margin-top:10px}'
            . 'table.kv td{border:1px solid #EFE2CE;padding:6px 9px;font-size:10.5pt}'
            . 'table.kv td:first-child{width:34%;background:#FBF4E7;color:#7A1414;font-weight:bold}'
            . '.pb{page-break-before:always}'
            // cover
            . '.cover{background:#7A1414;color:#fff;text-align:center;padding:70px 30px;border:3px solid #C9A227;border-radius:10px}'
            . '.cover .om{font-size:66pt;color:#FFD98A;line-height:1}'
            . '.cover .brand{font-size:20pt;font-weight:bold;letter-spacing:4px;color:#FFF;margin-top:6px}'
            . '.cover .brand-sub{font-size:9.5pt;color:#F3D9B0;margin-top:2px}'
            . '.cover .rule{border-top:1px solid rgba(255,255,255,.35);width:55%;margin:24px auto}'
            . '.cover-kicker{font-size:11pt;letter-spacing:2px;color:#FFD98A;text-transform:uppercase}'
            . '.cover-dev{font-size:30pt;font-weight:bold;color:#FFE7C4;margin-top:12px}'
            . '.cover-name{font-size:17pt;margin-top:2px}'
            . '.cover-meta{font-size:10pt;color:#F3D9B0;margin-top:20px}'
            // steps
            . '.step{border:1px solid #ECD9BD;border-radius:9px;padding:9px 11px;margin:8px 0;background:#FFFDF9}'
            . 'table.s-head{width:100%;border-collapse:collapse}'
            . '.s-no{width:26px;color:#fff;background:#E0730C;font-weight:bold;text-align:center;border-radius:20px;font-size:12pt}'
            . '.s-ttl{padding-left:9px}'
            . '.s-dev{color:#7A1414;font-weight:bold;font-size:13pt}.s-en{color:#6b5d4f;font-size:10.5pt}'
            . '.s-tags{text-align:right;white-space:nowrap}'
            . '.tg{font-size:8pt;font-weight:bold;border-radius:10px;padding:2px 7px;margin-left:3px}'
            . '.tg.ph{background:#EFE7FA;color:#5B21B6}.tg.req{background:#E7F6EC;color:#166534}.tg.opt{background:#FEF3C7;color:#92400E}.tg.dur{background:#EEF2F7;color:#475569}'
            . '.chant{background:#FFF8EC;border:1px solid #E7CE9E;border-radius:7px;padding:7px 10px;margin:7px 0}'
            . '.c-dev{font-size:13pt;line-height:1.8;color:#3a1d0a}.c-mean{font-size:10pt;color:#5a4326;margin-top:3px}'
            . '.s-meta{font-size:10pt;color:#475569;margin-top:3px}.s-meta b{color:#7A1414}'
            . '.s-kriya{font-size:10pt;margin-top:3px}.s-kriya b{color:#7A1414}'
            . '.s-mats{margin-top:5px}'
            . '.chip{background:#FFF1DF;border:1px solid #F0DCBB;color:#8a5a00;border-radius:10px;padding:1px 8px;font-size:9pt}'
            // materials
            . 'table.mat{width:100%;border-collapse:collapse;font-size:10.5pt}'
            . 'table.mat th{background:#FBEEDD;color:#7A1414;text-align:left;padding:6px 9px;border:1px solid #EAD9C2}'
            . 'table.mat td{padding:5px 9px;border:1px solid #EFE2CE}'
            // closing
            . '.closing{background:#3C1613;color:#fff;text-align:center;padding:90px 30px;border:3px solid #C9A227;border-radius:10px}'
            . '.closing .om2{font-size:60pt;color:#FFD98A}'
            . '.bless{font-size:20pt;color:#FFE7C4;margin-top:8px}'
            . '.bless-en{font-size:10.5pt;color:#F3D9B0;margin-top:4px}'
            . '.rule2{border-top:1px solid rgba(255,255,255,.3);width:50%;margin:26px auto}'
            . '.brand2{font-size:18pt;font-weight:bold;letter-spacing:5px}'
            . '.brand-sub2{font-size:9.5pt;color:#F3D9B0;margin-top:3px}'
            . '.foot2{font-size:8.5pt;color:#C6A98F;margin-top:26px}'
            . '</style></head><body>'
            . $cover
            . '<div class="pb"></div>' . $about
            . $stepsSection
            . '<div class="pb"></div>' . $matsSection
            . '<div class="pb"></div>' . $closing
            . '</body></html>';
    }

    /* ─────────────── Bhakti Setu — Scheduled e-Pooja (public) ─────────────── */

    /**
     * GET /api/public/bhakti-setu — scheduled e-poojas open for booking.
     * Lists `scheduled_poojas` whose status is OPEN (Booking Open) and whose
     * event date has not passed, each with its booked count / capacity and a
     * fully-booked flag. Full poojas stay listed until their event date.
     */
    public function bhaktiSetuList(): JsonResponse
    {
        $out = [];
        try {
            if (Schema::hasTable('scheduled_poojas')) {
                $this->sankalpEnsure();
                $this->reconcileScheduledStatuses();
                $today = date('Y-m-d');
                $q = DB::table('scheduled_poojas')
                    ->where(fn ($w) => $w->whereNull('is_active')->orWhere('is_active', 1))
                    ->where('status', 'OPEN');
                if (Schema::hasColumn('scheduled_poojas', 'deleted_at')) {
                    $q->whereNull('deleted_at');
                }
                if (Schema::hasColumn('scheduled_poojas', 'scheduled_date')) {
                    $q->where(fn ($w) => $w->whereNull('scheduled_date')->orWhere('scheduled_date', '>=', $today));
                }
                $rows = $q->orderByRaw('COALESCE(scheduled_date, "9999-12-31")')
                    ->orderByRaw('COALESCE(sort_order, 999999)')->orderBy('id')->limit(100)->get();
                foreach ($rows as $r) {
                    $out[] = $this->scheduledPoojaPayload((array) $r);
                }
            }
        } catch (Throwable) {
        }
        return $this->ok(['poojas' => $out]);
    }

    /** GET /api/public/bhakti-setu/{id} — one scheduled e-pooja for the detail page. */
    public function bhaktiSetuDetail(int $id): JsonResponse
    {
        try {
            if (Schema::hasTable('scheduled_poojas')) {
                $this->sankalpEnsure();
                $this->reconcileScheduledStatuses($id);
                $r = DB::table('scheduled_poojas')->where('id', $id)->first();
                if ($r) {
                    return $this->ok(['pooja' => $this->scheduledPoojaPayload((array) $r, true)]);
                }
            }
        } catch (Throwable) {
        }
        return $this->fail('Pooja not found.', 404);
    }

    /**
     * POST /api/public/bhakti-setu/{id}/book — book a sankalp in a scheduled
     * e-pooja. Public (no login). Enforces: the pooja is OPEN, the event date
     * has not passed, and the booking does not exceed the sankalp capacity.
     */
    public function bhaktiSetuBook(int $id, Request $request): JsonResponse
    {
        $d = $request->validate([
            'name'         => ['required', 'string', 'max:160'],
            'mobile'       => ['required', 'string', 'max:20'],
            'email'        => ['nullable', 'email', 'max:160'],
            'gotra'        => ['nullable', 'string', 'max:120'],
            'sankalp'      => ['nullable', 'string', 'max:1000'],
            'package_id'   => ['nullable', 'string', 'max:40'],
            'package_name' => ['nullable', 'string', 'max:120'],
            'persons'      => ['nullable', 'integer', 'min:1', 'max:100'],
            'amount'       => ['nullable', 'numeric', 'min:0'],
        ]);

        try {
            if (!Schema::hasTable('scheduled_poojas')) {
                return $this->fail('Bookings are not available right now.', 503);
            }
            $this->sankalpEnsure();
            $p = DB::table('scheduled_poojas')->where('id', $id)->first();
            if (!$p) {
                return $this->fail('Pooja not found.', 404);
            }
            $p = (array) $p;
            if ((string) ($p['status'] ?? '') !== 'OPEN') {
                return $this->fail('This pooja is not open for booking.', 422);
            }
            $today = date('Y-m-d');
            if (!empty($p['scheduled_date']) && (string) $p['scheduled_date'] < $today) {
                return $this->fail('This pooja date has passed.', 422);
            }

            // Resolve the selected package SERVER-SIDE (never trust a client price).
            // Capacity is now per-package, so it also carries booked/is_full.
            $svcBase = 0.0;
            try {
                $svcBase = (float) (DB::table('booking_services')->where('name', (string) ($p['pooja_name'] ?? ''))->value('base_fee') ?? 0);
            } catch (Throwable) {
            }
            $packages = $this->poojaPackages($id, (string) ($p['pooja_name'] ?? ''), $svcBase);
            if (!$packages) {
                return $this->fail('This pooja has no packages to book yet.', 422);
            }
            $chosen = null;
            if (!empty($d['package_id'])) {
                foreach ($packages as $pk) {
                    if ((string) $pk['id'] === (string) $d['package_id']) {
                        $chosen = $pk;
                        break;
                    }
                }
            }
            if (!$chosen && !empty($d['package_name'])) {
                foreach ($packages as $pk) {
                    if (strcasecmp((string) $pk['name'], (string) $d['package_name']) === 0) {
                        $chosen = $pk;
                        break;
                    }
                }
            }
            if (!$chosen) {
                $chosen = $packages[0]; // default to the first package
            }

            // Per-package capacity check.
            if (!empty($chosen['is_full'])) {
                return $this->fail('Sorry, the "' . $chosen['name'] . '" package is fully booked.', 409);
            }

            $ref = 'SNK-' . date('Y') . '-' . strtoupper(substr(bin2hex(random_bytes(4)), 0, 7));
            $insert = [
                'booking_ref'          => $ref,
                'scheduled_pooja_id'   => $id,
                'scheduled_pooja_code' => (string) ($p['pooja_name'] ?? ''),
                'yajman_name'          => $d['name'],
                'gotra'                => $d['gotra'] ?? null,
                'mobile'               => $d['mobile'] ?? null,
                'email'                => $d['email'] ?? null,
                'sankalp_text'         => $d['sankalp'] ?? null,
                'package_ref'          => (string) $chosen['id'],
                'package_name'         => (string) $chosen['name'],
                'persons'              => (int) $chosen['persons'],
                'amount'               => (float) $chosen['price'],
                'payment_status'       => 'PENDING',
                'fulfillment_status'   => 'BOOKED',
                'booked_on'            => $today,
            ];
            // Only keep columns that actually exist on the table.
            $cols = array_filter(array_keys($insert), fn ($c) => Schema::hasColumn('pooja_sankalp_bookings', $c));
            $insert = array_intersect_key($insert, array_flip($cols));
            if (Schema::hasColumn('pooja_sankalp_bookings', 'created_at')) {
                $insert['created_at'] = now();
            }
            DB::table('pooja_sankalp_bookings')->insert($insert);

            $cap = (int) ($chosen['capacity'] ?? 0);
            $pkgBooked = (int) ($chosen['booked'] ?? 0) + 1;
            return $this->ok([
                'booking_ref'  => $ref,
                'package'      => $chosen['name'],
                'booked'       => $pkgBooked,
                'capacity'     => $cap,
                'package_full' => $cap > 0 && $pkgBooked >= $cap,
                'message'      => 'Your sankalp is booked. The pandit will perform the pooja on your behalf.',
            ], 201);
        } catch (Throwable) {
            return $this->fail('Could not complete the booking. Please try again.', 500);
        }
    }

    /**
     * Normalise a scheduled_poojas row into the public Bhakti Setu shape.
     * $full adds the heavy detail-page fields (packages, benefits, process,
     * temple info, includes) — omitted from list cards for speed.
     */
    private function scheduledPoojaPayload(array $r, bool $full = false): array
    {
        $id       = (int) ($r['id'] ?? 0);
        $name     = (string) ($r['pooja_name'] ?? '');
        $date     = (string) ($r['scheduled_date'] ?? '');

        // Deity + linked Main Service (booking_services) — deity/base fee aren't stored here.
        $deity = '';
        $svc = null;
        $baseFee = 0.0;
        try {
            if ($name !== '' && Schema::hasTable('booking_services')) {
                $svc = DB::table('booking_services')->where('name', $name)->first();
                if ($svc) {
                    $baseFee = (float) ($svc->base_fee ?? 0);
                    if (!empty($svc->deity_id) && Schema::hasTable('deities')) {
                        $deity = (string) (DB::table('deities')->where('id', (int) $svc->deity_id)->value('name') ?? '');
                    }
                }
            }
        } catch (Throwable) {
        }

        // Capacity / price now live on the packages. Aggregate them for the pooja.
        $packages    = $this->poojaPackages($id, $name, $baseFee);
        $capTotal    = 0;
        $bookedTotal = 0;
        $hasUnlimited = false;
        $priceFrom   = null;
        $modes       = [];
        foreach ($packages as $pk) {
            $cap = (int) ($pk['capacity'] ?? 0);
            $capTotal    += $cap;
            $bookedTotal += (int) ($pk['booked'] ?? 0);
            if ($cap <= 0) {
                $hasUnlimited = true;
            }
            $pr = (float) ($pk['price'] ?? 0);
            if ($pr > 0 && ($priceFrom === null || $pr < $priceFrom)) {
                $priceFrom = $pr;
            }
            if (!empty($pk['mode'])) {
                $modes[(string) $pk['mode']] = true;
            }
        }
        // Fully booked only when every capacity-bound package is full and none is unlimited.
        $anyOpen = false;
        foreach ($packages as $pk) {
            if ((int) ($pk['capacity'] ?? 0) <= 0 || empty($pk['is_full'])) {
                $anyOpen = true;
                break;
            }
        }
        $isFull = count($packages) > 0 && !$anyOpen;

        $out = [
            'id'            => $id,
            'title'         => $name,
            'deity'         => $deity,
            'temple'        => (string) ($r['temple'] ?? ''),
            'date'          => $date,
            'tithi'         => $this->poojaTithi($date),
            'time'          => (string) ($r['scheduled_time'] ?? ''),
            'mode'          => implode(' · ', array_keys($modes)),
            'price'         => (float) ($priceFrom ?? 0),
            'price_from'    => $priceFrom,
            'capacity'      => $hasUnlimited ? 0 : $capTotal,
            'booked'        => $bookedTotal,
            'seats_left'    => $hasUnlimited ? null : max(0, $capTotal - $bookedTotal),
            'is_full'       => $isFull,
            'description'   => (string) ($r['description'] ?? ''),
            'image'         => (string) ($r['image'] ?? ''),
            'video_url'     => (string) ($r['video_url'] ?? ''),
            'pandit'        => implode(', ', $this->poojaPandits($id)),
            'livestream_url' => (string) ($r['livestream_url'] ?? ''),
            'status'        => (string) ($r['status'] ?? ''),
        ];

        if ($full) {
            $out['packages']    = $packages;
            $out['benefits']    = $this->serviceBenefits($svc);
            $out['process']     = $this->poojaProcess($svc);
            $out['temple_info'] = $this->templeInfo((string) ($r['temple'] ?? ''));
            $out['includes']    = $this->sankalpIncludes();
        } else {
            // Light: the list card still shows package availability + "from" price.
            $out['packages'] = $packages;
        }
        return $out;
    }

    /** Tithi label for a date (e.g. "Bhadrapada Krishna Ashtami"), memoised per request. */
    private function poojaTithi(string $date): string
    {
        static $cache = [];
        if ($date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            return '';
        }
        if (array_key_exists($date, $cache)) {
            return $cache[$date];
        }
        $label = '';
        try {
            [$lat, $lon, $tz] = $this->geo();
            $p = $this->panchang($date);
            $bits = array_filter([
                trim((string) ($p['masa'] ?? '')),
                trim((string) ($p['paksha'] ?? '')),
                trim((string) ($p['tithi'] ?? '')),
            ]);
            $label = trim(implode(' ', $bits));
        } catch (Throwable) {
        }
        return $cache[$date] = $label;
    }

    /**
     * Packages for a pooja: admin-managed rows from scheduled_pooja_packages if
     * any exist, otherwise a smart default set of four tiers derived from the
     * pooja's base price (not persisted).
     */
    private function poojaPackages(int $poojaId, string $name, float $base): array
    {
        $pkgs = [];
        // Admin-managed packages (each with its own price / capacity / mode).
        try {
            if (Schema::hasTable('scheduled_pooja_packages')) {
                $q = DB::table('scheduled_pooja_packages')->where('scheduled_pooja_id', $poojaId);
                if (Schema::hasColumn('scheduled_pooja_packages', 'is_active')) {
                    $q->where(fn ($w) => $w->whereNull('is_active')->orWhere('is_active', 1));
                }
                if (Schema::hasColumn('scheduled_pooja_packages', 'deleted_at')) {
                    $q->whereNull('deleted_at');
                }
                foreach ($q->orderByRaw('COALESCE(sort_order, 999999)')->orderBy('id')->get() as $row) {
                    $pkgs[] = [
                        'id'       => 'p' . (int) $row->id,
                        'name'     => (string) ($row->name ?? 'Package'),
                        'persons'  => (int) ($row->persons ?? 1),
                        'price'    => (float) ($row->price ?? 0),
                        'capacity' => (int) ($row->capacity ?? 0),
                        'mode'     => (string) ($row->mode ?? ''),
                        'tag'      => (string) ($row->tag ?? ''),
                    ];
                }
            }
        } catch (Throwable) {
        }

        // Smart defaults (Individual / Partner / Family / Joint Family) when no
        // packages are configured — derived from the linked service base fee.
        // Defaults are unlimited (capacity 0) and have no mode set.
        if (!$pkgs) {
            $b = $base > 0 ? $base : 851;
            $pkgs = [
                ['id' => 'd1', 'name' => 'Individual Puja',    'persons' => 1, 'price' => round($b),        'capacity' => 0, 'mode' => '', 'tag' => ''],
                ['id' => 'd2', 'name' => 'Partner Puja',       'persons' => 2, 'price' => round($b * 1.5),  'capacity' => 0, 'mode' => '', 'tag' => ''],
                ['id' => 'd3', 'name' => 'Family Puja + Bhog', 'persons' => 4, 'price' => round($b * 2.35), 'capacity' => 0, 'mode' => '', 'tag' => 'Popular'],
                ['id' => 'd4', 'name' => 'Joint Family Puja',  'persons' => 6, 'price' => round($b * 3.5),  'capacity' => 0, 'mode' => '', 'tag' => ''],
            ];
        }

        // Per-package booked counts (one grouped query), then availability flags.
        $counts = [];
        try {
            if (Schema::hasTable('pooja_sankalp_bookings') && Schema::hasColumn('pooja_sankalp_bookings', 'package_ref')) {
                $cq = DB::table('pooja_sankalp_bookings')
                    ->where('scheduled_pooja_id', $poojaId)
                    ->select('package_ref', DB::raw('COUNT(*) as n'));
                if (Schema::hasColumn('pooja_sankalp_bookings', 'payment_status')) {
                    $cq->where('payment_status', '!=', 'REFUNDED');
                }
                foreach ($cq->groupBy('package_ref')->get() as $c) {
                    $counts[(string) $c->package_ref] = (int) $c->n;
                }
            }
        } catch (Throwable) {
        }
        foreach ($pkgs as &$pk) {
            $cap = (int) $pk['capacity'];
            $booked = (int) ($counts[$pk['id']] ?? 0);
            $pk['booked']     = $booked;
            $pk['seats_left'] = $cap > 0 ? max(0, $cap - $booked) : null;
            $pk['is_full']    = $cap > 0 && $booked >= $cap;
        }
        unset($pk);
        return $pkgs;
    }

    /** Benefits list for the detail page — from the linked service's highlights. */
    private function serviceBenefits(?object $svc): array
    {
        $out = [];
        try {
            $raw = (string) ($svc->highlights ?? '');
            foreach (preg_split('/\r\n|\r|\n/', $raw) ?: [] as $ln) {
                $ln = trim((string) $ln);
                if ($ln !== '') {
                    $out[] = $ln;
                }
            }
        } catch (Throwable) {
        }
        return $out;
    }

    /** Ritual process (steps) for the detail page — from the linked service's pooja_steps. */
    private function poojaProcess(?object $svc): array
    {
        $out = [];
        try {
            $slug = (string) ($svc->slug ?? '');
            if ($slug !== '' && Schema::hasTable('pooja_steps')) {
                $q = DB::table('pooja_steps')->where('pooja_code', $slug);
                if (Schema::hasColumn('pooja_steps', 'deleted_at')) {
                    $q->whereNull('deleted_at');
                }
                if (Schema::hasColumn('pooja_steps', 'is_active')) {
                    $q->where('is_active', 1);
                }
                foreach ($q->orderByRaw('COALESCE(step_no, 999999)')->orderBy('id')->limit(20)->get() as $s) {
                    $out[] = [
                        'title'     => (string) ($s->title ?? ''),
                        'title_dev' => (string) ($s->title_dev ?? ''),
                    ];
                }
            }
        } catch (Throwable) {
        }
        return $out;
    }

    /** Temple info block for the detail page — from the Temples master by name. */
    private function templeInfo(string $temple): array
    {
        try {
            if ($temple !== '' && Schema::hasTable('temples')) {
                $t = DB::table('temples')->where('name', $temple)->first();
                if ($t) {
                    $t = (array) $t;
                    return [
                        'name'        => (string) ($t['name'] ?? $temple),
                        'name_dev'    => (string) ($t['name_dev'] ?? ''),
                        'description' => (string) ($t['description'] ?? ''),
                        'city'        => (string) ($t['city'] ?? ''),
                        'state'       => (string) ($t['state'] ?? ''),
                        'image_url'   => (string) ($t['image_url'] ?? ''),
                    ];
                }
            }
        } catch (Throwable) {
        }
        return $temple !== '' ? ['name' => $temple] : [];
    }

    /** What every sankalp booking includes (shown above the package selector). */
    private function sankalpIncludes(): array
    {
        return [
            "Your name and gotra will be recited by an experienced Panditji during the puja.",
            "You receive guided mantras and step-by-step instructions to join the puja from home.",
            "A complete video of the puja and offerings is shared on your WhatsApp.",
            "A free Aashirwad box with tirth prasad is delivered to your home if you opt in.",
        ];
    }

    /** Count active (non-refunded) sankalp bookings for a scheduled pooja. */
    private function sankalpBookedCount(int $poojaId): int
    {
        try {
            if (!Schema::hasTable('pooja_sankalp_bookings') || !Schema::hasColumn('pooja_sankalp_bookings', 'scheduled_pooja_id')) {
                return 0;
            }
            $q = DB::table('pooja_sankalp_bookings')->where('scheduled_pooja_id', $poojaId);
            if (Schema::hasColumn('pooja_sankalp_bookings', 'payment_status')) {
                $q->where('payment_status', '!=', 'REFUNDED');
            }
            return (int) $q->count();
        } catch (Throwable) {
            return 0;
        }
    }

    /** Ensure the sankalp bookings table exists and can link to a scheduled pooja by id. */
    private function sankalpEnsure(): void
    {
        try {
            if (!Schema::hasTable('pooja_sankalp_bookings')) {
                DB::statement("CREATE TABLE IF NOT EXISTS `pooja_sankalp_bookings` (
                    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                    `booking_ref` VARCHAR(40) NOT NULL UNIQUE,
                    `scheduled_pooja_id` INT UNSIGNED DEFAULT NULL,
                    `scheduled_pooja_code` VARCHAR(190) DEFAULT NULL,
                    `yajman_name` VARCHAR(190) DEFAULT NULL,
                    `gotra` VARCHAR(120) DEFAULT NULL,
                    `mobile` VARCHAR(30) DEFAULT NULL,
                    `email` VARCHAR(190) DEFAULT NULL,
                    `sankalp_text` TEXT DEFAULT NULL,
                    `amount` DECIMAL(12,2) NOT NULL DEFAULT 0,
                    `payment_status` VARCHAR(20) NOT NULL DEFAULT 'PENDING',
                    `fulfillment_status` VARCHAR(30) NOT NULL DEFAULT 'BOOKED',
                    `prasad_tracking` VARCHAR(120) DEFAULT NULL,
                    `booked_on` DATE DEFAULT NULL,
                    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    KEY `idx_sched`(`scheduled_pooja_id`,`payment_status`)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
            }
            // Additive columns (never break a live table).
            foreach ([
                'scheduled_pooja_id' => "ADD COLUMN `scheduled_pooja_id` INT UNSIGNED DEFAULT NULL",
                'package_ref'        => "ADD COLUMN `package_ref` VARCHAR(40) DEFAULT NULL",
                'package_name'       => "ADD COLUMN `package_name` VARCHAR(120) DEFAULT NULL",
                'persons'            => "ADD COLUMN `persons` INT DEFAULT 1",
            ] as $col => $ddl) {
                if (!Schema::hasColumn('pooja_sankalp_bookings', $col)) {
                    try {
                        DB::statement("ALTER TABLE `pooja_sankalp_bookings` $ddl");
                    } catch (Throwable) {
                    }
                }
            }
        } catch (Throwable) {
        }
    }

    /**
     * Status flow: a scheduled pooja starts as SCHEDULED and automatically
     * becomes OPEN (Booking Open) once it has a pandit assigned AND at least one
     * package configured. Only promotes SCHEDULED (or blank) rows — never
     * downgrades or touches LIVE/COMPLETED/CANCELLED. $onlyId limits it to one row.
     */
    private function reconcileScheduledStatuses(?int $onlyId = null): void
    {
        try {
            if (!Schema::hasTable('scheduled_poojas')
                || !Schema::hasTable('scheduled_pooja_packages')
                || !Schema::hasTable('scheduled_pooja_pandits')) {
                return;
            }
            $q = DB::table('scheduled_poojas')
                ->where(function ($w) {
                    $w->where('status', 'SCHEDULED')->orWhereNull('status')->orWhere('status', '');
                })
                // at least one package…
                ->whereIn('id', function ($sub) {
                    $sub->select('scheduled_pooja_id')->from('scheduled_pooja_packages');
                })
                // …and at least one pandit assigned.
                ->whereIn('id', function ($sub) {
                    $sub->select('scheduled_pooja_id')->from('scheduled_pooja_pandits');
                });
            if ($onlyId !== null) {
                $q->where('id', $onlyId);
            }
            $q->update(['status' => 'OPEN']);
        } catch (Throwable) {
        }
    }

    /** Assigned pandit names for a scheduled pooja (from scheduled_pooja_pandits). */
    private function poojaPandits(int $poojaId): array
    {
        try {
            if (!Schema::hasTable('scheduled_pooja_pandits')) {
                return [];
            }
            return DB::table('scheduled_pooja_pandits')->where('scheduled_pooja_id', $poojaId)
                ->orderByRaw('COALESCE(sort_order, id)')->orderBy('id')
                ->pluck('pandit_name')->filter()->map(fn ($n) => (string) $n)->values()->all();
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * GET /api/public/numerology-services — every ACTIVE service the admin has
     * filed under the Numerology category, for the detailed numerology page's
     * premium add-on list. Unlike the landing grid this is NOT gated on
     * show_on_landing, and it returns slug + input_fields so the page can render
     * the admin-defined client input form for each service.
     */
    public function numerologyServices(): JsonResponse
    {
        // Self-provision the five Life-Forecast services + peripherals + readings.
        \App\Services\Numerology\LifeForecastCatalog::ensure();
        return $this->ok(['services' => $this->servicesForCategory('numerolog')]);
    }

    /** GET /api/public/astrology-services — Astrology-category catalogue services. */
    public function astrologyServices(): JsonResponse
    {
        return $this->ok(['services' => $this->servicesForCategory('astro')]);
    }

    /** GET /api/public/palmistry-services — Palmistry-category catalogue services. */
    public function palmistryServices(): JsonResponse
    {
        return $this->ok(['services' => $this->servicesForCategory('palm')]);
    }

    /** GET /api/public/vastu-services — Vastu-category catalogue services. */
    public function vastuServices(): JsonResponse
    {
        return $this->ok(['services' => $this->servicesForCategory('vastu')]);
    }

    /**
     * Active services whose category name contains the given needle
     * (case-insensitive). Returns admin-managed fields incl. slug, input_fields
     * and price when those columns exist on the live table.
     */
    private function servicesForCategory(string $needle): array
    {
        // Booking Catalogue (booking_services) is the single source of services.
        \App\Support\BookingCatalog::ensure();
        $table = 'booking_services';
        try {
            $want = [
                'id', 'name', 'name_dev', 'slug', 'icon', 'category', 'tag_line', 'description',
                'highlights', 'badge', 'duration_min', 'is_online', 'is_offline',
                'base_fee', 'fee_type', 'input_fields', 'sort_order', 'landing_order',
            ];
            $cols = array_values(array_filter($want, fn ($c) => Schema::hasColumn($table, $c)));
            if (!in_array('name', $cols, true)) {
                return [];
            }
            $hasInputs = in_array('input_fields', $cols, true);

            $q = DB::table($table)
                ->where('is_active', 1)
                ->whereRaw('LOWER(COALESCE(category, \'\')) LIKE ?', ['%' . strtolower($needle) . '%']);
            if (Schema::hasColumn($table, 'sort_order')) {
                $q->orderBy('sort_order');
            }
            $rows = $q->orderBy('name')->limit(60)->get($cols);

            // Territory currency for this visitor (INR / USD).
            \App\Services\Pricing\PriceBook::ensureSchema();
            $cur = \App\Services\Pricing\CurrencyResolver::resolve(request());

            return $rows->map(function ($r) use ($hasInputs, $cur) {
                $a = (array) $r;
                if ($hasInputs) {
                    $decoded = !empty($a['input_fields']) ? json_decode((string) $a['input_fields'], true) : [];
                    $a['input_fields'] = is_array($decoded) ? $decoded : [];
                }
                // Alias so the numerology page (expects price/price_type) keeps working.
                $a['price']      = $a['base_fee'] ?? null;
                $a['price_type'] = $a['fee_type'] ?? null;
                // Territory-resolved price for the visitor's currency (base INR kept as `price`).
                $a = \App\Services\Pricing\PriceBook::decorate($a, 'service', $a['id'] ?? null, $a['base_fee'] ?? 0, $cur);
                // Peripheral services mapped under this main service, in their own
                // sort order (rendered as sections inside the service's tab).
                $a['peripherals'] = isset($a['id'])
                    ? array_map(function ($p) use ($cur) {
                        $p = (array) $p;
                        $baseInr = $p['base_fee'] ?? $p['price'] ?? 0;
                        return \App\Services\Pricing\PriceBook::decorate($p, 'service', $p['id'] ?? null, $baseInr, $cur);
                    }, \App\Support\BookingCatalog::addonsForService((int) $a['id']))
                    : [];
                return $a;
            })->all();
        } catch (Throwable) {
            return [];
        }
    }

    /** GET /api/public/testimonials — featured testimonials. */
    public function testimonialsList(): JsonResponse
    {
        return $this->ok(['testimonials' => $this->testimonials()]);
    }

    /**
     * POST /api/inquiry — public contact / inquiry capture (writes to inquiries).
     * Mirrors the PHP `inquiry` route.
     */
    public function inquiry(Request $request): JsonResponse
    {
        $data = $request->validate([
            'name'    => ['required', 'string', 'max:120'],
            'mobile'  => ['sometimes', 'nullable', 'string', 'max:20'],
            'email'   => ['sometimes', 'nullable', 'email', 'max:190'],
            'subject' => ['sometimes', 'nullable', 'string', 'max:200'],
            'message' => ['sometimes', 'nullable', 'string', 'max:4000'],
        ]);
        if (empty($data['mobile']) && empty($data['email'])) {
            return $this->fail('Please provide a mobile number or email so we can reach you.', 422);
        }

        try {
            $id = (int) DB::table('inquiries')->insertGetId([
                'name'     => $data['name'],
                'mobile'   => $data['mobile'] ?? null,
                'email'    => $data['email'] ?? null,
                'subject'  => $data['subject'] ?? null,
                'message'  => isset($data['message']) ? Html::sanitize($data['message']) : null,
                'source'   => 'website',
                'status'   => 'new',
                'is_active' => 1,
            ]);
            try {
                Audit::log('inquiries', $id, 'create', null, ['source' => 'website']);
            } catch (Throwable) {
            }
            // Redirect website inquiries into the unified Seva Exchange inbox.
            try {
                \App\Support\SevaExchange::record('inquiry', $id, [
                    'source_ref'     => 'INQ-' . $id,
                    'yajman_name'    => $data['name'],
                    'contact_mobile' => $data['mobile'] ?? null,
                    'contact_email'  => $data['email'] ?? null,
                    'subject'        => $data['subject'] ?? 'Website inquiry',
                    'message'        => $data['message'] ?? null,
                    'status'         => 'enquiry',
                ]);
            } catch (Throwable) {
            }
            return $this->ok(['ok' => true, 'id' => $id, 'message' => 'Thank you! Your inquiry has been received — we will get back to you shortly.'], 201);
        } catch (Throwable $e) {
            return $this->fail('Could not submit your inquiry right now. Please try again.', 500);
        }
    }

    // ── data blocks ────────────────────────────────────────────────────────────

    private function siteBlock(): array
    {
        return [
            'name'    => (string) Setting::get('site_name', 'Vedic Platform'),
            'tagline' => (string) Setting::get('tagline', 'Authentic Vedic spiritual services'),
            'about'   => (string) Setting::get('about_text', 'Authentic Vedic spiritual services rooted in ancient Indian tradition.'),
            'phone'   => (string) Setting::get('contact_phone', ''),
            'email'   => (string) Setting::get('contact_email', ''),
            'address' => (string) Setting::get('contact_address', ''),
        ];
    }

    private function services(): array
    {
        // Landing "offering services" now come from the Booking Catalogue
        // (booking_services) — the single source that replaced the Service Master.
        \App\Support\BookingCatalog::ensure();
        $table = 'booking_services';
        try {
            $want = ['id', 'slug', 'name', 'name_dev', 'icon', 'category', 'tag_line', 'description', 'highlights', 'base_fee', 'fee_type', 'badge', 'duration_min', 'is_online', 'is_offline', 'input_fields'];
            $cols = array_values(array_filter($want, fn ($c) => Schema::hasColumn($table, $c)));
            if (!in_array('name', $cols, true)) {
                return [];
            }
            $hasInputs = in_array('input_fields', $cols, true);
            $rows = DB::table($table)
                ->where('is_active', 1)
                ->when(Schema::hasColumn($table, 'show_on_landing'), fn ($b) => $b->where('show_on_landing', 1))
                ->when(Schema::hasColumn($table, 'category'), fn ($b) => $b->orderBy('category'))
                ->orderByRaw('COALESCE(landing_order, sort_order, 0)')
                ->orderBy('name')
                ->limit(48)
                ->get($cols);
            return $rows->map(function ($r) use ($hasInputs) {
                $a = (array) $r;
                // Decode the admin-defined client input fields JSON into a real array.
                if ($hasInputs) {
                    $decoded = !empty($a['input_fields']) ? json_decode((string) $a['input_fields'], true) : [];
                    $a['input_fields'] = is_array($decoded) ? $decoded : [];
                }
                // Alias booking fee to the price keys the landing template expects.
                $a['price']      = $a['base_fee'] ?? null;
                $a['price_type'] = $a['fee_type'] ?? null;
                return $a;
            })->all();
        } catch (Throwable) {
            return [];
        }
    }

    private function testimonials(): array
    {
        try {
            $rows = DB::table('testimonials')
                ->where('is_active', 1)
                ->orderByDesc('is_featured')
                ->orderByDesc('id')
                ->limit(12)
                ->get(['name', 'city', 'service', 'rating', 'content']);
            return $rows->map(fn ($r) => (array) $r)->all();
        } catch (Throwable) {
            return [];
        }
    }

    private function numerologyBlock(): array
    {
        return [
            'title'    => (string) Setting::get('num_section_title', 'Numerology Intelligence Engine'),
            'subtitle' => (string) Setting::get('num_section_subtitle', 'Discover the power of numbers — birth chart to Lo Shu grid analysis'),
            'heading'  => (string) Setting::get('num_intro_heading', 'What Our Numerology Report Covers'),
        ];
    }
}
