<?php
require __DIR__ . '/bootstrap.php';

use App\Core\Auth;
use App\Core\Csrf;
use App\Core\Database;
use App\Core\Response;
use App\Core\Security;
use App\Services\AttendanceService;
use App\Services\CouponService;

use App\Services\NotificationService;
use App\Services\UltraMsgService;
use App\Services\WhatsAppQueueService;
$pdo = Database::connection();
$input = request_input();
$method = $_SERVER['REQUEST_METHOD'];
$action = $input['action'] ?? 'list';
$currentUser = Auth::user();

function pyrk_datetime_ts(string $date, string $time = '00:00'): int
{
    $time = trim($time);
    if ($time === '') $time = '00:00';
    $time = substr($time, 0, 5);
    $value = trim($date) . ' ' . $time . ':00';
    $ts = strtotime($value);
    if ($ts === false) {
        Response::error('Invalid walk date/time.');
    }
    return $ts;
}

function pyrk_validate_walk_schedule(array $input): void
{
    $walkDate = trim((string)($input['walk_date'] ?? ''));
    $startTime = trim((string)($input['start_time'] ?? ''));
    $finishTime = trim((string)($input['expected_finish_time'] ?? ''));

    if ($walkDate === '' || $startTime === '' || $finishTime === '') {
        return;
    }

    $startTs = pyrk_datetime_ts($walkDate, $startTime);
    $finishTs = pyrk_datetime_ts($walkDate, $finishTime);

    if ($startTs >= $finishTs) {
        Response::error('Start time must be before expected finish time.');
    }

    $registrationDeadline = trim((string)($input['registration_deadline'] ?? ''));
    if ($registrationDeadline !== '') {
        $deadlineTs = strtotime(str_replace('T', ' ', $registrationDeadline));
        if ($deadlineTs === false) {
            Response::error('Invalid registration deadline.');
        }
        if ($deadlineTs > $startTs) {
            Response::error('Registration deadline cannot be after the walk starts.');
        }
        if (substr(str_replace('T', ' ', $registrationDeadline), 0, 10) > $walkDate) {
            Response::error('Registration deadline date cannot be after the walk date.');
        }
    }

    // Voting deadline is intentionally allowed after the walk date.
}


function pyrk_walk_payment_input(array $input): array
{
    $model = strtolower(trim((string)($input['payment_model'] ?? 'free')));
    if (!in_array($model, ['free', 'paid'], true)) {
        $model = 'free';
    }

    $currency = strtoupper(trim((string)($input['payment_currency'] ?? 'LBP')));
    if (!in_array($currency, ['LBP', 'USD', 'EUR'], true)) {
        $currency = 'LBP';
    }

    $amountRaw = str_replace(',', '', (string)($input['payment_amount'] ?? '0'));
    $amount = is_numeric($amountRaw) ? round((float)$amountRaw, 2) : 0.0;
    if ($amount < 0) {
        Response::error('Payment amount cannot be negative.');
    }

    if ($model === 'free') {
        $amount = 0.0;
    }

    if ($model === 'paid' && $amount <= 0) {
        Response::error('Paid walks must have an amount greater than 0.');
    }

    return [
        'payment_model' => $model,
        'payment_amount' => $amount,
        'payment_currency' => $currency,
    ];
}


function pyrk_walk_shift_relative_datetime(?string $value, string $sourceDate, string $sourceStartTime, string $targetDate, string $targetStartTime): ?string
{
    $value = trim((string)$value);
    if ($value === '') return null;

    try {
        $sourceStart = new DateTimeImmutable(trim($sourceDate) . ' ' . substr(trim($sourceStartTime), 0, 5));
        $targetStart = new DateTimeImmutable(trim($targetDate) . ' ' . substr(trim($targetStartTime), 0, 5));
        $sourceValue = new DateTimeImmutable(str_replace('T', ' ', $value));
        $offsetSeconds = $sourceValue->getTimestamp() - $sourceStart->getTimestamp();
        $modifier = ($offsetSeconds >= 0 ? '+' : '') . $offsetSeconds . ' seconds';
        return $targetStart->modify($modifier)->format('Y-m-d H:i:s');
    } catch (Throwable) {
        return null;
    }
}


function pyrk_user_registration(PDO $pdo, int $walkId, ?array $user): ?array
{
    if (!$user || empty($user['id'])) return null;
    $stmt = $pdo->prepare('SELECT wr.id, wr.registration_status, wr.registered_at, a.status AS attendance_status, a.check_in_time, a.finish_time, a.verified_km FROM walk_registrations wr LEFT JOIN attendance a ON a.walk_id = wr.walk_id AND a.user_id = wr.user_id WHERE wr.walk_id = ? AND wr.user_id = ? AND wr.registration_status IN ("registered","approved") LIMIT 1');
    $stmt->execute([$walkId, (int)$user['id']]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

function pyrk_user_waiver_signed(PDO $pdo, int $walkId, ?array $user): bool
{
    if (!$user || empty($user['id'])) return false;
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM waiver_signatures WHERE walk_id = ? AND user_id = ? AND accepted_terms = 1');
    $stmt->execute([$walkId, (int)$user['id']]);
    return (int)$stmt->fetchColumn() > 0;
}

function pyrk_member_walk_is_finished(?array $registration): bool
{
    if (!$registration) return false;
    return in_array(($registration['attendance_status'] ?? ''), ['finished', 'manual_verified'], true)
        || (!empty($registration['finish_time']) && ($registration['attendance_status'] ?? '') !== 'partially_completed');
}

function pyrk_member_walk_is_partial(?array $registration): bool
{
    if (!$registration) return false;
    return ($registration['attendance_status'] ?? '') === 'partially_completed';
}



function pyrk_walk_team_table_exists(PDO $pdo): bool
{
    static $exists = null;
    if ($exists !== null) return $exists;

    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "walk_staff_assignments"');
        $stmt->execute();
        return $exists = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable) {
        return $exists = false;
    }
}

function pyrk_walk_user_can_view_all_walks(?array $user): bool
{
    if (!$user) return false;

    // Full access to all walks must be explicit.
    // Creating or editing a walk does NOT mean the user sees every walk.
    // View all walks must be explicit.
    // Event manager users no longer get all walks automatically by role.
    return Auth::hasPermission('walks.manage', $user)
        || Auth::hasPermission('walks.view_all', $user)
        || in_array($user['role_name'] ?? '', ['super_admin'], true);
}

function pyrk_walk_user_can_view_assigned_walks(?array $user): bool
{
    if (!$user) return false;

    return pyrk_walk_user_can_view_all_walks($user)
        || Auth::hasPermission('walks.view_assigned', $user)
        || Auth::hasPermission('walks.create', $user)
        || Auth::hasPermission('walks.edit', $user)
        || Auth::hasPermission('walks.assign_staff', $user)
        || Auth::hasPermission('attendance.manage', $user);
}

function pyrk_walk_user_assigned_role(PDO $pdo, int $walkId, int $userId): ?string
{
    if ($walkId <= 0 || $userId <= 0 || !pyrk_walk_team_table_exists($pdo)) return null;

    try {
        $stmt = $pdo->prepare('SELECT assignment_role FROM walk_staff_assignments WHERE walk_id = ? AND user_id = ? ORDER BY FIELD(assignment_role, "manager", "head_walker", "walker") LIMIT 1');
        $stmt->execute([$walkId, $userId]);
        $role = $stmt->fetchColumn();
        return $role ? (string)$role : null;
    } catch (Throwable) {
        return null;
    }
}

function pyrk_walk_user_is_assigned(PDO $pdo, int $walkId, ?array $user): bool
{
    if (!$user || empty($user['id'])) return false;
    return pyrk_walk_user_assigned_role($pdo, $walkId, (int)$user['id']) !== null;
}

function pyrk_walk_user_has_any_assignment(PDO $pdo, ?array $user): bool
{
    if (!$user || empty($user['id']) || !pyrk_walk_team_table_exists($pdo)) return false;

    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM walk_staff_assignments WHERE user_id = ?');
        $stmt->execute([(int)$user['id']]);
        return (int)$stmt->fetchColumn() > 0;
    } catch (Throwable) {
        return false;
    }
}


function pyrk_walk_user_is_creator(PDO $pdo, int $walkId, ?array $user): bool
{
    if (!$user || empty($user['id']) || $walkId <= 0) return false;

    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM walks WHERE id = ? AND created_by = ?');
        $stmt->execute([$walkId, (int)$user['id']]);
        return (int)$stmt->fetchColumn() > 0;
    } catch (Throwable) {
        return false;
    }
}

function pyrk_walk_user_has_own_walk_full_access(PDO $pdo, int $walkId, ?array $user): bool
{
    if (!$user || $walkId <= 0) return false;

    return pyrk_walk_user_can_view_all_walks($user)
        || pyrk_walk_user_is_creator($pdo, $walkId, $user)
        || pyrk_walk_user_assigned_role($pdo, $walkId, (int)($user['id'] ?? 0)) === 'manager';
}

function pyrk_walk_user_has_any_own_walk(PDO $pdo, ?array $user): bool
{
    if (!$user || empty($user['id'])) return false;

    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM walks WHERE created_by = ?');
        $stmt->execute([(int)$user['id']]);
        return (int)$stmt->fetchColumn() > 0;
    } catch (Throwable) {
        return false;
    }
}


function pyrk_walk_user_has_registration_or_attendance(PDO $pdo, int $walkId, ?array $user): bool
{
    if (!$user || empty($user['id']) || $walkId <= 0) return false;

    try {
        $stmt = $pdo->prepare('SELECT
                EXISTS(SELECT 1 FROM walk_registrations wr WHERE wr.walk_id = ? AND wr.user_id = ? AND wr.registration_status IN ("registered", "approved"))
                OR EXISTS(SELECT 1 FROM attendance a WHERE a.walk_id = ? AND a.user_id = ? AND (a.check_in_time IS NOT NULL OR a.status IN ("checked_in", "started", "finished", "partially_completed", "manual_verified")))');
        $stmt->execute([$walkId, (int)$user['id'], $walkId, (int)$user['id']]);
        return (int)$stmt->fetchColumn() === 1;
    } catch (Throwable) {
        return false;
    }
}


function pyrk_walk_user_can_access_walk(PDO $pdo, array $walk, ?array $user): bool
{
    $walkId = (int)($walk['id'] ?? 0);
    if ($walkId <= 0) return false;

    if (pyrk_walk_user_can_view_all_walks($user)) return true;
    if (pyrk_walk_user_is_creator($pdo, $walkId, $user)) return true;
    if (pyrk_walk_user_is_assigned($pdo, $walkId, $user)) return true;

    // Draft walks are an admin review state and must never appear in the member portal.
    if (strtolower(trim((string)($walk['status'] ?? ''))) === 'draft') return false;

    // A member who already registered or checked in must keep access to the walk,
    // even if invitations or team assignments are later adjusted.
    if (pyrk_walk_user_has_registration_or_attendance($pdo, $walkId, $user)) return true;

    // Staff users must not see unrelated open walks.
    // They can see all only with walks.view_all / walks.manage, otherwise only created/assigned walks.
    if (pyrk_is_staff($user)) {
        return false;
    }

    $accessType = (string)($walk['access_type'] ?? 'open');
    if ($accessType === 'open') return true;

    if ($user && !empty($user['id'])) {
        return pyrk_member_is_invited($pdo, $walkId, (int)$user['id']);
    }

    return false;
}

function pyrk_walk_user_can_manage_team(PDO $pdo, int $walkId, ?array $user): bool
{
    if (!$user || $walkId <= 0) return false;

    // Global full access is explicit only.
    if (Auth::hasPermission('walks.manage', $user)) return true;

    // The new permission controls who can assign staff, but it must not expose unrelated walks by itself.
    // To assign staff across all walks, grant walks.assign_staff + walks.view_all or walks.manage.
    if (Auth::hasPermission('walks.assign_staff', $user)) {
        return pyrk_walk_user_can_view_all_walks($user)
            || pyrk_walk_user_is_creator($pdo, $walkId, $user)
            || pyrk_walk_user_is_assigned($pdo, $walkId, $user);
    }

    // Creator and assigned Manager have full access only to this specific walk.
    return pyrk_walk_user_is_creator($pdo, $walkId, $user)
        || pyrk_walk_user_assigned_role($pdo, $walkId, (int)$user['id']) === 'manager';
}

function pyrk_walk_team_assignments(PDO $pdo, int $walkId): array
{
    $out = [
        'manager' => [],
        'head_walker' => [],
        'walker' => [],
    ];

    if ($walkId <= 0 || !pyrk_walk_team_table_exists($pdo)) return $out;

    try {
        $stmt = $pdo->prepare('SELECT
                wsa.id,
                wsa.walk_id,
                wsa.user_id,
                wsa.assignment_role,
                wsa.assigned_at,
                u.full_name,
                u.email,
                u.mobile,
                u.profile_photo_path,
                r.name AS role_name
            FROM walk_staff_assignments wsa
            JOIN users u ON u.id = wsa.user_id
            JOIN roles r ON r.id = u.role_id
            WHERE wsa.walk_id = ?
            ORDER BY FIELD(wsa.assignment_role, "manager", "head_walker", "walker"), u.full_name ASC');
        $stmt->execute([$walkId]);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $role = (string)($row['assignment_role'] ?? 'walker');
            if (!isset($out[$role])) $out[$role] = [];
            $out[$role][] = $row;
        }
    } catch (Throwable) {
        // Keep app working if migration was not run.
    }

    return $out;
}

function pyrk_walk_staff_options(PDO $pdo): array
{
    try {
        $stmt = $pdo->query('SELECT u.id, u.full_name, u.email, u.mobile, u.profile_photo_path, u.status, r.name AS role_name
            FROM users u
            JOIN roles r ON r.id = u.role_id
            WHERE u.status = "active"
            ORDER BY
              CASE r.name
                WHEN "super_admin" THEN 1
                WHEN "event_manager" THEN 2
                WHEN "walk_leader" THEN 3
                WHEN "member" THEN 5
                ELSE 4
              END,
              u.full_name ASC');
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    } catch (Throwable) {
        return [];
    }
}

function pyrk_walk_staff_ids_from_input(mixed $value): array
{
    if (is_string($value)) {
        $decoded = json_decode($value, true);
        if (is_array($decoded)) {
            $value = $decoded;
        } else {
            $value = preg_split('/\s*,\s*/', $value);
        }
    }

    if (!is_array($value)) return [];

    return array_values(array_unique(array_filter(array_map('intval', $value))));
}

function pyrk_walk_sync_staff_assignments(PDO $pdo, int $walkId, array $managerIds, array $headWalkerIds, array $walkerIds, int $adminId): void
{
    $managerIds = array_slice(array_values(array_unique(array_filter(array_map('intval', $managerIds)))), 0, 1);
    $headWalkerIds = array_slice(array_values(array_unique(array_filter(array_map('intval', $headWalkerIds)))), 0, 1);
    $walkerIds = array_values(array_unique(array_filter(array_map('intval', $walkerIds))));

    // Do not duplicate the same user as normal walker if they are already manager/head walker.
    $reserved = array_flip(array_merge($managerIds, $headWalkerIds));
    $walkerIds = array_values(array_filter($walkerIds, static fn($id) => !isset($reserved[(int)$id])));

    $pdo->prepare('DELETE FROM walk_staff_assignments WHERE walk_id = ?')->execute([$walkId]);

    $stmt = $pdo->prepare('INSERT INTO walk_staff_assignments (walk_id, user_id, assignment_role, assigned_by, assigned_at, created_at, updated_at)
        VALUES (?, ?, ?, ?, NOW(), NOW(), NOW())');

    foreach ($managerIds as $id) {
        $stmt->execute([$walkId, $id, 'manager', $adminId]);
    }
    foreach ($headWalkerIds as $id) {
        $stmt->execute([$walkId, $id, 'head_walker', $adminId]);
    }
    foreach ($walkerIds as $id) {
        $stmt->execute([$walkId, $id, 'walker', $adminId]);
    }
}



function pyrk_walks_table_exists(PDO $pdo, string $table): bool
{
    static $cache = [];
    if (array_key_exists($table, $cache)) return $cache[$table];

    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?');
        $stmt->execute([$table]);
        return $cache[$table] = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable) {
        return $cache[$table] = false;
    }
}

function pyrk_walks_column_exists(PDO $pdo, string $table, string $column): bool
{
    static $cache = [];
    $key = $table . '.' . $column;
    if (array_key_exists($key, $cache)) return $cache[$key];

    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?');
        $stmt->execute([$table, $column]);
        return $cache[$key] = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable) {
        return $cache[$key] = false;
    }
}

function pyrk_walk_visible_sql(PDO $pdo, string $alias = 'w'): string
{
    $a = preg_replace('/[^a-zA-Z0-9_]/', '', $alias) ?: 'w';

    $parts = [
        'LOWER(TRIM(COALESCE(' . $a . '.status, ""))) NOT IN ("deleted","archived","removed","remove","trash","trashed","cancelled","canceled","inactive")',
    ];

    foreach (['deleted_at', 'removed_at', 'archived_at', 'cancelled_at', 'canceled_at'] as $col) {
        if (pyrk_walks_column_exists($pdo, 'walks', $col)) {
            $parts[] = $a . '.' . $col . ' IS NULL';
        }
    }

    foreach (['is_deleted', 'deleted', 'is_archived', 'archived', 'is_removed'] as $col) {
        if (pyrk_walks_column_exists($pdo, 'walks', $col)) {
            $parts[] = 'COALESCE(' . $a . '.' . $col . ', 0) = 0';
        }
    }

    foreach (['deleted_by', 'removed_by', 'archived_by'] as $col) {
        if (pyrk_walks_column_exists($pdo, 'walks', $col)) {
            $parts[] = $a . '.' . $col . ' IS NULL';
        }
    }

    // Current DB dump shows deleted walks in audit_logs as action = walk_deleted.
    // Important: if a walk id was deleted earlier and then reused by MySQL/import,
    // an old audit row must NOT hide the new active walk.
    // Therefore an audit deletion only hides a walk when the deletion happened
    // after the current walk row was created.
    if (pyrk_walks_table_exists($pdo, 'audit_logs')) {
        $auditTimeGuard = '';
        if (pyrk_walks_column_exists($pdo, 'audit_logs', 'created_at') && pyrk_walks_column_exists($pdo, 'walks', 'created_at')) {
            $auditTimeGuard = ' AND COALESCE(al_deleted_walk.created_at, "1000-01-01 00:00:00") >= COALESCE(' . $a . '.created_at, "1000-01-01 00:00:00")';
        }

        $parts[] = 'NOT EXISTS (
            SELECT 1 FROM audit_logs al_deleted_walk
            WHERE al_deleted_walk.entity_type = "walks"
              AND al_deleted_walk.entity_id = ' . $a . '.id
              AND al_deleted_walk.action IN ("walk_deleted", "walk_removed")
              ' . $auditTimeGuard . '
        )';
    }

    return '(' . implode(' AND ', $parts) . ')';
}

function pyrk_walk_is_visible(PDO $pdo, int $walkId): bool
{
    if ($walkId <= 0) return false;
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM walks w WHERE w.id = ? AND ' . pyrk_walk_visible_sql($pdo, 'w'));
    $stmt->execute([$walkId]);
    return (int)$stmt->fetchColumn() > 0;
}

function pyrk_walk_archive_columns(PDO $pdo): array
{
    return [
        'archived_at' => pyrk_walks_column_exists($pdo, 'walks', 'archived_at'),
        'archived_by' => pyrk_walks_column_exists($pdo, 'walks', 'archived_by'),
        'archive_reason' => pyrk_walks_column_exists($pdo, 'walks', 'archive_reason'),
        'archived_previous_status' => pyrk_walks_column_exists($pdo, 'walks', 'archived_previous_status'),
    ];
}

function pyrk_walk_archive_where_sql(PDO $pdo, string $alias = 'w'): string
{
    $a = preg_replace('/[^a-zA-Z0-9_]/', '', $alias) ?: 'w';
    $parts = ['LOWER(TRIM(COALESCE(' . $a . '.status, ""))) = "archived"'];
    if (pyrk_walks_column_exists($pdo, 'walks', 'archived_at')) {
        $parts[] = $a . '.archived_at IS NOT NULL';
    }
    if (pyrk_walks_column_exists($pdo, 'walks', 'is_archived')) {
        $parts[] = 'COALESCE(' . $a . '.is_archived, 0) = 1';
    }
    if (pyrk_walks_column_exists($pdo, 'walks', 'archived')) {
        $parts[] = 'COALESCE(' . $a . '.archived, 0) = 1';
    }
    return '(' . implode(' OR ', $parts) . ')';
}

function pyrk_walk_user_can_archive(PDO $pdo, int $walkId, array $user): bool
{
    return pyrk_walk_user_has_own_walk_full_access($pdo, $walkId, $user)
        || (Auth::hasPermission('walks.edit', $user) && pyrk_walk_user_can_view_all_walks($user))
        || (Auth::hasPermission('walks.manage', $user) && pyrk_walk_user_can_view_all_walks($user))
        || (Auth::hasPermission('walks.delete', $user) && pyrk_walk_user_can_view_all_walks($user));
}


function pyrk_is_staff(?array $user): bool
{
    return $user && in_array($user['role_name'] ?? '', ['super_admin','event_manager','walk_leader'], true);
}

function pyrk_can_manage_walks(?array $user): void
{
    if (!$user || !Auth::hasPermission('walks.manage', $user)) {
        Response::error('You do not have permission to manage walks.', 403);
    }
}

function pyrk_can_create_walks(?array $user): void
{
    if (!$user || (!Auth::hasPermission('walks.create', $user) && !Auth::hasPermission('walks.manage', $user))) {
        Response::error('You do not have permission to create walks.', 403);
    }
}

function pyrk_can_edit_walks(?array $user): void
{
    if (!$user || (!Auth::hasPermission('walks.edit', $user) && !Auth::hasPermission('walks.manage', $user))) {
        Response::error('You do not have permission to edit walks.', 403);
    }
}

function pyrk_can_delete_walks(?array $user): void
{
    if (!$user || !Auth::hasPermission('walks.delete', $user)) {
        Response::error('You do not have permission to delete walks.', 403);
    }
}

function pyrk_can_view_walk_registrants(?array $user, ?int $walkId = null, ?PDO $pdo = null): void
{
    if (!$user) {
        Response::error('Please login again.', 401);
    }

    // Global users can view all registrants. Everyone else must be creator or assigned to this exact walk.
    if (pyrk_walk_user_can_view_all_walks($user)) {
        return;
    }

    if ($walkId && $pdo) {
        $allowed = pyrk_walk_user_is_creator($pdo, (int)$walkId, $user)
            || pyrk_walk_user_is_assigned($pdo, (int)$walkId, $user);
        if ($allowed) return;
    }

    Response::error('You can view registrants only for walks created by you or assigned to you.', 403);
}

function pyrk_can_unregister_walk_member(?array $user): void
{
    if (!$user) {
        Response::error('Please login again.', 401);
    }

    $allowed = Auth::hasPermission('walks.unregister', $user)
        || Auth::hasPermission('walks.manage', $user);

    if (!$allowed) {
        Response::error('You do not have permission to unregister members from walks. Enable walks.unregister.', 403);
    }
}

function pyrk_can_open_walk_permissions(?array $user): void
{
    if (!$user) {
        Response::error('Please login again.', 401);
    }

    if (!pyrk_walk_user_can_view_all_walks($user)) {
        Response::error('You do not have global permission to edit unrelated walk permissions.', 403);
    }
}

function pyrk_invitation_summary(PDO $pdo, int $walkId): array
{
    $memberStmt = $pdo->prepare('SELECT wi.id, wi.user_id, wi.status, u.full_name, u.email, u.mobile, u.profile_photo_path FROM walk_invitations wi JOIN users u ON u.id = wi.user_id WHERE wi.walk_id = ? AND wi.invitation_type = "member" ORDER BY u.full_name ASC');
    $memberStmt->execute([$walkId]);
    $groupStmt = $pdo->prepare('SELECT wi.id, wi.group_id, wi.status, g.group_name, g.description, (SELECT COUNT(*) FROM member_group_members mgm WHERE mgm.group_id = g.id) AS member_count FROM walk_invitations wi JOIN member_groups g ON g.id = wi.group_id WHERE wi.walk_id = ? AND wi.invitation_type = "group" ORDER BY g.group_name ASC');
    $groupStmt->execute([$walkId]);
    return ['members' => $memberStmt->fetchAll(), 'groups' => $groupStmt->fetchAll()];
}

function pyrk_member_is_invited(PDO $pdo, int $walkId, int $userId): bool
{
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM walk_invitations wi WHERE wi.walk_id = ? AND wi.status = "invited" AND (wi.user_id = ? OR wi.group_id IN (SELECT mgm.group_id FROM member_group_members mgm WHERE mgm.user_id = ?))');
    $stmt->execute([$walkId, $userId, $userId]);
    return (int)$stmt->fetchColumn() > 0;
}

function pyrk_sync_walk_invitations(PDO $pdo, int $walkId, array $memberIds, array $groupIds, int $adminId): array
{
    $memberIds = array_values(array_unique(array_filter(array_map('intval', $memberIds))));
    $groupIds = array_values(array_unique(array_filter(array_map('intval', $groupIds))));

    $existingMembers = [];
    $existingStmt = $pdo->prepare('SELECT user_id, status, notified_at FROM walk_invitations WHERE walk_id = ? AND invitation_type = "member" AND user_id IS NOT NULL');
    $existingStmt->execute([$walkId]);
    foreach ($existingStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $existingMembers[(int)$row['user_id']] = $row;
    }

    $existingGroups = [];
    $existingGroupStmt = $pdo->prepare('SELECT group_id, status, notified_at FROM walk_invitations WHERE walk_id = ? AND invitation_type = "group" AND group_id IS NOT NULL');
    $existingGroupStmt->execute([$walkId]);
    foreach ($existingGroupStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $existingGroups[(int)$row['group_id']] = $row;
    }

    $newMemberIds = [];
    $reactivatedMemberIds = [];
    $newGroupIds = [];
    $reactivatedGroupIds = [];

    if ($memberIds) {
        $placeholders = implode(',', array_fill(0, count($memberIds), '?'));
        $pdo->prepare("DELETE FROM walk_invitations WHERE walk_id = ? AND invitation_type = 'member' AND user_id IS NOT NULL AND user_id NOT IN ({$placeholders})")->execute(array_merge([$walkId], $memberIds));
    } else {
        $pdo->prepare('DELETE FROM walk_invitations WHERE walk_id = ? AND invitation_type = "member" AND user_id IS NOT NULL')->execute([$walkId]);
    }

    if ($groupIds) {
        $placeholders = implode(',', array_fill(0, count($groupIds), '?'));
        $pdo->prepare("DELETE FROM walk_invitations WHERE walk_id = ? AND invitation_type = 'group' AND group_id IS NOT NULL AND group_id NOT IN ({$placeholders})")->execute(array_merge([$walkId], $groupIds));
    } else {
        $pdo->prepare('DELETE FROM walk_invitations WHERE walk_id = ? AND invitation_type = "group" AND group_id IS NOT NULL')->execute([$walkId]);
    }

    $insertMember = $pdo->prepare('INSERT INTO walk_invitations (walk_id, user_id, invitation_type, invited_by, invited_at, status, created_at, updated_at, notified_at, notified_email_status) VALUES (?, ?, "member", ?, NOW(), "invited", NOW(), NOW(), NULL, NULL)');
    $keepMember = $pdo->prepare('UPDATE walk_invitations SET invited_by = ?, status = "invited", updated_at = NOW() WHERE walk_id = ? AND invitation_type = "member" AND user_id = ?');
    $reactivateMember = $pdo->prepare('UPDATE walk_invitations SET invited_by = ?, invited_at = NOW(), status = "invited", notified_at = NULL, notified_email_status = NULL, updated_at = NOW() WHERE walk_id = ? AND invitation_type = "member" AND user_id = ?');
    foreach ($memberIds as $memberId) {
        if (!isset($existingMembers[$memberId])) {
            $insertMember->execute([$walkId, $memberId, $adminId]);
            $newMemberIds[] = $memberId;
            continue;
        }
        if (($existingMembers[$memberId]['status'] ?? '') !== 'invited') {
            $reactivateMember->execute([$adminId, $walkId, $memberId]);
            $reactivatedMemberIds[] = $memberId;
        } else {
            $keepMember->execute([$adminId, $walkId, $memberId]);
        }
    }

    $insertGroup = $pdo->prepare('INSERT INTO walk_invitations (walk_id, group_id, invitation_type, invited_by, invited_at, status, created_at, updated_at, notified_at, notified_email_status) VALUES (?, ?, "group", ?, NOW(), "invited", NOW(), NOW(), NULL, NULL)');
    $keepGroup = $pdo->prepare('UPDATE walk_invitations SET invited_by = ?, status = "invited", updated_at = NOW() WHERE walk_id = ? AND invitation_type = "group" AND group_id = ?');
    $reactivateGroup = $pdo->prepare('UPDATE walk_invitations SET invited_by = ?, invited_at = NOW(), status = "invited", notified_at = NULL, notified_email_status = NULL, updated_at = NOW() WHERE walk_id = ? AND invitation_type = "group" AND group_id = ?');
    foreach ($groupIds as $groupId) {
        if (!isset($existingGroups[$groupId])) {
            $insertGroup->execute([$walkId, $groupId, $adminId]);
            $newGroupIds[] = $groupId;
            continue;
        }
        if (($existingGroups[$groupId]['status'] ?? '') !== 'invited') {
            $reactivateGroup->execute([$adminId, $walkId, $groupId]);
            $reactivatedGroupIds[] = $groupId;
        } else {
            $keepGroup->execute([$adminId, $walkId, $groupId]);
        }
    }

    return [
        'new_member_ids' => $newMemberIds,
        'reactivated_member_ids' => $reactivatedMemberIds,
        'new_group_ids' => $newGroupIds,
        'reactivated_group_ids' => $reactivatedGroupIds,
        'has_new' => (bool)(count($newMemberIds) + count($reactivatedMemberIds) + count($newGroupIds) + count($reactivatedGroupIds)),
    ];
}


function pyrk_walk_notification_html(string $message): string
{
    $message = trim($message);
    if ($message === '') return '';

    $safe = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
    $safe = preg_replace_callback(
        '~https?://[^\s<]+~i',
        static function (array $match): string {
            $url = (string)$match[0];
            return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>';
        },
        $safe
    ) ?? $safe;

    return '<p>' . nl2br($safe) . '</p>';
}

function pyrk_walk_notification_name_parts(?string $fullName): array
{
    $fullName = trim((string)preg_replace('/\s+/', ' ', (string)$fullName));
    if ($fullName === '') {
        return ['first_name' => 'Member', 'last_name' => '', 'full_name' => 'Member'];
    }

    $parts = preg_split('/\s+/', $fullName) ?: [];
    $first = (string)(array_shift($parts) ?: 'Member');
    $last = trim(implode(' ', $parts));

    return [
        'first_name' => $first,
        'last_name' => $last,
        'full_name' => trim($first . ' ' . $last),
    ];
}

function pyrk_walk_notification_public_base_url(): string
{
    $configured = rtrim(trim((string)($GLOBALS['config']['base_url'] ?? '')), '/');
    if ($configured !== '') {
        if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && str_starts_with($configured, 'http://')) {
            $configured = 'https://' . substr($configured, 7);
        }
        return $configured;
    }

    $host = preg_replace('/[^A-Za-z0-9.:-]/', '', (string)($_SERVER['HTTP_HOST'] ?? ''));
    if ($host === '') return '';

    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $scriptName = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? '/steps/api/walks.php'));
    $apiDir = rtrim(dirname($scriptName), '/');
    $rootDir = rtrim(dirname($apiDir), '/');

    return $scheme . '://' . $host . ($rootDir !== '' ? $rootDir : '') . '/public';
}

function pyrk_walk_notification_walk_url(int $walkId): string
{
    $baseUrl = pyrk_walk_notification_public_base_url();
    return $baseUrl !== '' ? $baseUrl . '/walk.php?id=' . $walkId : '';
}

function pyrk_walk_notification_date(string $value): string
{
    $value = trim($value);
    if ($value === '') return '';
    try {
        return (new DateTimeImmutable($value))->format('d/m/Y');
    } catch (Throwable) {
        return $value;
    }
}

function pyrk_walk_notification_variables(array $walk, array $recipient, int $walkId, bool $includeDollarAliases = false): array
{
    $names = pyrk_walk_notification_name_parts((string)($recipient['full_name'] ?? ''));
    $distance = (float)($walk['total_km'] ?? 0);
    $distanceText = rtrim(rtrim(number_format($distance, 2, '.', ''), '0'), '.');

    $values = [
        '{first_name}' => $names['first_name'],
        '{last_name}' => $names['last_name'],
        '{full_name}' => $names['full_name'],
        '{walk_title}' => trim((string)($walk['title'] ?? 'Pyrk walk')),
        '{walk_date}' => pyrk_walk_notification_date((string)($walk['walk_date'] ?? '')),
        '{walk_time}' => substr((string)($walk['start_time'] ?? ''), 0, 5),
        '{route_name}' => trim((string)($walk['route_name'] ?? '')),
        '{distance_km}' => $distanceText,
        '{walk_link}' => pyrk_walk_notification_walk_url($walkId),
    ];
    if ($includeDollarAliases) {
        foreach ($values as $token => $value) $values['$' . trim($token, '{}') . '$'] = $value;
    }
    return $values;
}

function pyrk_walk_notification_render(string $template, array $walk, array $recipient, int $walkId): string
{
    $rendered = strtr($template, pyrk_walk_notification_variables($walk, $recipient, $walkId, false));
    $rendered = preg_replace('/[ \t]+\n/', "\n", $rendered) ?? $rendered;
    $rendered = preg_replace('/\n{4,}/', "\n\n\n", $rendered) ?? $rendered;
    $unresolved = pyrk_walk_notification_tokens($rendered);
    if ($unresolved) throw new RuntimeException('Unresolved Push notification variable(s): ' . implode(', ', $unresolved));
    return trim($rendered);
}

function pyrk_walk_whatsapp_render(string $template, array $walk, array $recipient, int $walkId): string
{
    $rendered = strtr($template, pyrk_walk_notification_variables($walk, $recipient, $walkId, true));
    $rendered = preg_replace('/[ \t]+\n/', "\n", $rendered) ?? $rendered;
    $rendered = preg_replace('/\n{4,}/', "\n\n\n", $rendered) ?? $rendered;
    $unresolved = pyrk_walk_notification_tokens($rendered);
    if ($unresolved) throw new RuntimeException('Unresolved WhatsApp variable(s): ' . implode(', ', $unresolved));
    return trim($rendered);
}

function pyrk_walk_general_notification_recipients(PDO $pdo, int $walkId, string $audience): array
{
    $audience = in_array($audience, ['all', 'invited'], true) ? $audience : 'all';

    if ($audience === 'invited') {
        $sql = "SELECT DISTINCT u.id
            FROM users u
            JOIN roles r ON r.id = u.role_id
            LEFT JOIN walk_invitations wi_member
              ON wi_member.walk_id = ?
             AND wi_member.status = 'invited'
             AND wi_member.invitation_type = 'member'
             AND wi_member.user_id = u.id
            LEFT JOIN member_group_members mgm
              ON mgm.user_id = u.id
            LEFT JOIN walk_invitations wi_group
              ON wi_group.walk_id = ?
             AND wi_group.status = 'invited'
             AND wi_group.invitation_type = 'group'
             AND wi_group.group_id = mgm.group_id
            WHERE r.name = 'member'
              AND u.status = 'active'
              AND (wi_member.id IS NOT NULL OR wi_group.id IS NOT NULL)
            ORDER BY u.full_name ASC";
        $stmt = $pdo->prepare($sql);
        $stmt->execute([$walkId, $walkId]);
    } else {
        $stmt = $pdo->query("SELECT DISTINCT u.id
            FROM users u
            JOIN roles r ON r.id = u.role_id
            WHERE r.name = 'member' AND u.status = 'active'
            ORDER BY u.full_name ASC");
    }

    return array_values(array_unique(array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN))));
}

function pyrk_walk_notification_presets_installed(PDO $pdo): bool
{
    static $installed = null;
    if ($installed !== null) return $installed;
    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?');
        $stmt->execute(['push_notification_presets']);
        return $installed = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable) {
        return $installed = false;
    }
}

function pyrk_walk_notification_allowed_tokens(): array
{
    return ['{first_name}','{last_name}','{full_name}','{walk_title}','{walk_date}','{walk_time}','{route_name}','{distance_km}','{walk_link}'];
}

function pyrk_walk_notification_tokens(string $value): array
{
    preg_match_all('/\{[a-zA-Z0-9_]+\}|\$[a-zA-Z0-9_]+\$/', $value, $matches);
    return array_values(array_unique($matches[0] ?? []));
}

function pyrk_walk_notification_unknown_tokens(string ...$values): array
{
    $found = [];
    foreach ($values as $value) $found = array_merge($found, pyrk_walk_notification_tokens($value));
    return array_values(array_diff(array_unique($found), pyrk_walk_notification_allowed_tokens()));
}

function pyrk_walk_notification_resolve_templates(PDO $pdo, array $walkInput, string $legacySubject, string $legacyMessage): array
{
    $subject = trim((string)($walkInput['walk_notify_subject'] ?? ''));
    $message = trim((string)($walkInput['walk_notify_message'] ?? ''));
    $presetId = (int)($walkInput['walk_notify_push_preset_id'] ?? 0);
    $preset = null;

    if ($presetId > 0 && pyrk_walk_notification_presets_installed($pdo)) {
        try {
            $stmt = $pdo->prepare('SELECT id, subject_template, message_template FROM push_notification_presets WHERE id = ? AND is_active = 1 LIMIT 1');
            $stmt->execute([$presetId]);
            $preset = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
        } catch (Throwable) {
            $preset = null;
        }
    }

    if ($subject === '' && $preset) $subject = trim((string)($preset['subject_template'] ?? ''));
    if ($message === '' && $preset) $message = trim((string)($preset['message_template'] ?? ''));

    if (($subject === '' || $message === '') && pyrk_walk_notification_presets_installed($pdo)) {
        return [
            'ok' => false,
            'subject' => $subject,
            'message' => $message,
            'preset_id' => $presetId,
            'error' => 'Choose and apply an active Push notification preset, or write the subject and message before sending.',
        ];
    }

    if ($subject === '') $subject = $legacySubject;
    if ($message === '') $message = $legacyMessage;

    $unknown = pyrk_walk_notification_unknown_tokens($subject, $message);
    if ($unknown) {
        return [
            'ok' => false,
            'subject' => $subject,
            'message' => $message,
            'preset_id' => $presetId,
            'error' => 'Unknown variable(s): ' . implode(', ', $unknown) . '. Use only the approved Push notification variables.',
        ];
    }

    return [
        'ok' => true,
        'subject' => $subject,
        'message' => $message,
        'preset_id' => $preset ? (int)$preset['id'] : $presetId,
    ];
}

function pyrk_walk_whatsapp_presets_installed(PDO $pdo): bool
{
    static $installed = null;
    if ($installed !== null) return $installed;
    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?');
        $stmt->execute(['whatsapp_message_presets']);
        return $installed = ((int)$stmt->fetchColumn() > 0);
    } catch (Throwable) {
        return $installed = false;
    }
}

function pyrk_walk_whatsapp_unknown_tokens(string ...$values): array
{
    $allowed = pyrk_walk_notification_allowed_tokens();
    foreach (pyrk_walk_notification_allowed_tokens() as $token) $allowed[] = '$' . trim($token, '{}') . '$';
    $found = [];
    foreach ($values as $value) $found = array_merge($found, pyrk_walk_notification_tokens($value));
    return array_values(array_diff(array_unique($found), $allowed));
}

function pyrk_walk_whatsapp_resolve_templates(PDO $pdo, array $walkInput, string $legacySubject, string $legacyMessage): array
{
    $presetId = (int)($walkInput['walk_notify_whatsapp_preset_id'] ?? 0);
    $preset = null;
    $installed = pyrk_walk_whatsapp_presets_installed($pdo);

    if ($installed) {
        if ($presetId > 0) {
            $stmt = $pdo->prepare('SELECT id, headline_template, message_template FROM whatsapp_message_presets WHERE id = ? AND is_active = 1 LIMIT 1');
            $stmt->execute([$presetId]);
            $preset = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
            if (!$preset) {
                return ['ok' => false, 'preset_id' => $presetId, 'error' => 'The selected WhatsApp preset is missing or inactive. Reload the walk form and choose an active WhatsApp preset.'];
            }
        } else {
            $useCase = !empty($walkInput['id'])
                ? 'walk_update'
                : ((string)($walkInput['access_type'] ?? 'open') === 'invitation_only' ? 'invitation_only' : 'open_walk');
            $stmt = $pdo->prepare('SELECT id, headline_template, message_template FROM whatsapp_message_presets WHERE is_active = 1 ORDER BY (use_case = ?) DESC, (use_case = ? AND is_default = 1) DESC, is_default DESC, sort_order ASC, id ASC LIMIT 1');
            $stmt->execute([$useCase, $useCase]);
            $preset = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
        }
    }

    $subject = $preset ? trim((string)$preset['headline_template']) : $legacySubject;
    $message = $preset ? trim((string)$preset['message_template']) : $legacyMessage;
    if ($subject === '' || $message === '') {
        return ['ok' => false, 'preset_id' => $presetId, 'error' => 'Choose an active WhatsApp preset before enabling WhatsApp delivery.'];
    }
    $unknown = pyrk_walk_whatsapp_unknown_tokens($subject, $message);
    if ($unknown) {
        return ['ok' => false, 'preset_id' => $presetId, 'error' => 'Unknown WhatsApp variable(s): ' . implode(', ', $unknown) . '.'];
    }
    return [
        'ok' => true,
        'subject' => $subject,
        'message' => $message,
        'preset_id' => $preset ? (int)$preset['id'] : 0,
    ];
}

function pyrk_send_walk_general_notification(PDO $pdo, int $walkId, array $walkInput, int $adminId): array
{
    if (empty($walkInput['walk_notify_enabled'])) {
        return ['enabled' => false, 'sent' => 0, 'audience' => 'none'];
    }

    $walkStmt = $pdo->prepare('SELECT w.id, w.title, w.walk_date, w.start_time, w.access_type, r.route_name, r.total_km FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? LIMIT 1');
    $walkStmt->execute([$walkId]);
    $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
    if (!$walk) {
        return ['enabled' => true, 'sent' => 0, 'failed' => 1, 'error' => 'Walk not found.'];
    }

    $requestedAudience = (string)($walkInput['walk_notify_audience'] ?? 'auto');
    if (!in_array($requestedAudience, ['auto', 'all', 'invited'], true)) $requestedAudience = 'auto';
    $audience = $requestedAudience === 'auto'
        ? ((string)($walk['access_type'] ?? 'open') === 'invitation_only' ? 'invited' : 'all')
        : $requestedAudience;

    $recipientIds = pyrk_walk_general_notification_recipients($pdo, $walkId, $audience);
    if (!$recipientIds) {
        return ['enabled' => true, 'sent' => 0, 'audience' => $audience, 'message' => 'No recipients found.'];
    }

    $placeholders = implode(',', array_fill(0, count($recipientIds), '?'));
    $recipientStmt = $pdo->prepare("SELECT id, full_name FROM users WHERE id IN ({$placeholders}) AND status = 'active' ORDER BY full_name ASC");
    $recipientStmt->execute($recipientIds);
    $recipients = $recipientStmt->fetchAll(PDO::FETCH_ASSOC);
    if (!$recipients) {
        return ['enabled' => true, 'sent' => 0, 'audience' => $audience, 'message' => 'No active recipients found.'];
    }

    $legacySubject = $audience === 'invited'
        ? 'You are invited: {walk_title}'
        : 'New Pyrk walk: {walk_title}';
    $legacyMessage = $audience === 'invited'
        ? "Hello {first_name},\n\nYou are invited to join an upcoming Pyrk walk.\n\n🥾 {walk_title}\n📅 {walk_date}\n🕒 {walk_time}\n📍 {route_name}\n📏 {distance_km} KM\n\nPlease review the walk details, sign the waiver if required, and register here:\n{walk_link}\n\nSee you on the trail!\nPyrk Walking Club"
        : "Hello {first_name},\n\nA new Pyrk walk is now available.\n\n🥾 {walk_title}\n📅 {walk_date}\n🕒 {walk_time}\n📍 {route_name}\n📏 {distance_km} KM\n\nView the walk details, sign the waiver if required, and register here:\n{walk_link}\n\nSee you on the trail!\nPyrk Walking Club";
    $templates = pyrk_walk_notification_resolve_templates($pdo, $walkInput, $legacySubject, $legacyMessage);
    if (empty($templates['ok'])) {
        return ['enabled' => true, 'sent' => 0, 'audience' => $audience, 'blocked' => true, 'message' => (string)($templates['error'] ?? 'Notification content is required.')];
    }
    $subjectTemplate = (string)$templates['subject'];
    $messageTemplate = (string)$templates['message'];
    $presetId = (int)($templates['preset_id'] ?? 0);

    $insert = $pdo->prepare('INSERT INTO notifications (user_id, walk_id, channel, subject, message, status, scheduled_at, sent_at, error_message, read_at, created_at) VALUES (?, ?, "in_app", ?, ?, "sent", NOW(), NOW(), NULL, NULL, NOW())');
    $sent = 0;
    foreach ($recipients as $recipient) {
        $subject = pyrk_walk_notification_render($subjectTemplate, $walk, $recipient, $walkId);
        $messageText = pyrk_walk_notification_render($messageTemplate, $walk, $recipient, $walkId);
        $insert->execute([(int)$recipient['id'], $walkId, $subject, pyrk_walk_notification_html($messageText)]);
        $sent++;
    }

    try {
        Security::audit($adminId, 'walk_general_notification_sent', 'walks', $walkId, [
            'audience' => $audience,
            'requested_audience' => $requestedAudience,
            'subject_template' => $subjectTemplate,
            'preset_id' => $presetId,
            'recipients' => $sent,
        ]);
    } catch (Throwable) {}

    return ['enabled' => true, 'sent' => $sent, 'audience' => $audience];
}

function pyrk_walk_general_notification_message(array $result): string
{
    if (empty($result['enabled'])) return '';
    $sent = (int)($result['sent'] ?? 0);
    $audience = (string)($result['audience'] ?? 'members');
    if ($sent > 0) {
        return ' General notification sent to ' . $sent . ' ' . ($audience === 'invited' ? 'invited member(s).' : 'member(s).');
    }
    $reason = trim((string)($result['message'] ?? ''));
    if ($reason !== '') return ' General notification was not sent: ' . $reason;
    return ' General notification was enabled, but no recipients were found.';
}

function pyrk_queue_walk_whatsapp_notification(PDO $pdo, int $walkId, array $walkInput, int $adminId): array
{
    if (empty($walkInput['walk_notify_enabled']) || empty($walkInput['walk_notify_whatsapp'])) {
        return ['enabled' => false, 'queued' => 0, 'duplicates' => 0, 'invalid' => 0, 'audience' => 'none'];
    }

    if (strtolower((string)($walkInput['status'] ?? 'open')) === 'draft') {
        return [
            'enabled' => true,
            'queued' => 0,
            'duplicates' => 0,
            'invalid' => 0,
            'audience' => 'none',
            'blocked' => true,
            'message' => 'WhatsApp was not queued because the walk was saved as a draft.',
        ];
    }

    try {
        $walkStmt = $pdo->prepare('SELECT w.id, w.title, w.walk_date, w.start_time, w.access_type, r.route_name, r.total_km FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? LIMIT 1');
        $walkStmt->execute([$walkId]);
        $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
        if (!$walk) {
            return ['enabled' => true, 'queued' => 0, 'blocked' => true, 'message' => 'Walk not found for WhatsApp queueing.'];
        }

        // Editing safety: do not broadcast the same walk repeatedly by mistake.
        // A deliberate repeat is still possible through the explicit admin override.
        $isWalkEdit = (int)($walkInput['id'] ?? 0) > 0;
        $allowRecentRepeat = !empty($walkInput['walk_notify_whatsapp_repeat']);
        if ($isWalkEdit && !$allowRecentRepeat) {
            try {
                $recentStmt = $pdo->prepare('SELECT created_at, new_value_json
                    FROM audit_logs
                    WHERE action = "walk_whatsapp_notification_queued"
                      AND entity_type = "walks"
                      AND entity_id = ?
                      AND created_at >= DATE_SUB(NOW(), INTERVAL 60 MINUTE)
                    ORDER BY id DESC
                    LIMIT 1');
                $recentStmt->execute([$walkId]);
                $recent = $recentStmt->fetch(PDO::FETCH_ASSOC);
                if ($recent) {
                    $details = json_decode((string)($recent['new_value_json'] ?? ''), true);
                    $previousQueued = is_array($details) ? (int)($details['queued'] ?? 0) : 1;
                    if ($previousQueued > 0) {
                        return [
                            'enabled' => true,
                            'queued' => 0,
                            'duplicates' => $previousQueued,
                            'invalid' => 0,
                            'audience' => 'none',
                            'blocked' => true,
                            'message' => 'Duplicate protection: this walk already queued a WhatsApp broadcast within the last 60 minutes (' . (string)$recent['created_at'] . '). Check “Send another update anyway” only when a second broadcast is intentional.',
                        ];
                    }
                }
            } catch (Throwable) {
                // Audit lookup must never prevent an otherwise valid notification.
            }
        }

        $requestedAudience = (string)($walkInput['walk_notify_audience'] ?? 'auto');
        if (!in_array($requestedAudience, ['auto', 'all', 'invited'], true)) $requestedAudience = 'auto';
        $audience = $requestedAudience === 'auto'
            ? ((string)($walk['access_type'] ?? 'open') === 'invitation_only' ? 'invited' : 'all')
            : $requestedAudience;

        $recipientIds = pyrk_walk_general_notification_recipients($pdo, $walkId, $audience);
        if (!$recipientIds) {
            return ['enabled' => true, 'queued' => 0, 'duplicates' => 0, 'invalid' => 0, 'audience' => $audience, 'message' => 'No WhatsApp recipients were found.'];
        }

        $placeholders = implode(',', array_fill(0, count($recipientIds), '?'));
        $recipientsStmt = $pdo->prepare("SELECT id, full_name, mobile FROM users WHERE id IN ({$placeholders}) AND status = 'active' ORDER BY full_name ASC");
        $recipientsStmt->execute($recipientIds);
        $rows = $recipientsStmt->fetchAll(PDO::FETCH_ASSOC);

        $provider = new UltraMsgService();
        $queue = new WhatsAppQueueService($pdo, $provider);
        if (!$queue->schemaReady()) {
            return [
                'enabled' => true,
                'queued' => 0,
                'blocked' => true,
                'audience' => $audience,
                'message' => 'WhatsApp queue is not installed. Import migration 041_ultramsg_whatsapp_queue.sql.',
            ];
        }

        $uniqueRecipients = [];
        $invalid = 0;
        foreach ($rows as $row) {
            $mobile = $provider->normalizeMobile((string)($row['mobile'] ?? ''));
            if ($mobile === null) {
                $invalid++;
                continue;
            }
            if (!isset($uniqueRecipients[$mobile])) {
                $uniqueRecipients[$mobile] = [
                    'user_id' => (int)$row['id'],
                    'mobile' => $mobile,
                    'full_name' => (string)($row['full_name'] ?? ''),
                ];
            }
        }

        $recipientCount = count($uniqueRecipients);
        if ($recipientCount === 0) {
            return [
                'enabled' => true,
                'queued' => 0,
                'duplicates' => 0,
                'invalid' => $invalid,
                'audience' => $audience,
                'message' => 'No valid WhatsApp numbers were found for this audience.',
            ];
        }

        $limits = $provider->limits();
        $maxPerBatch = (int)($limits['max_per_batch'] ?? 50);
        if ($recipientCount > $maxPerBatch) {
            return [
                'enabled' => true,
                'queued' => 0,
                'duplicates' => 0,
                'invalid' => $invalid,
                'audience' => $audience,
                'blocked' => true,
                'recipient_count' => $recipientCount,
                'message' => "WhatsApp was not queued: {$recipientCount} recipients exceed the safe per-walk limit of {$maxPerBatch}. Use Manual Notifications or increase the configured limit carefully.",
            ];
        }

        $overview = $queue->overview();
        $remainingToday = (int)($overview['remaining_today'] ?? 0);
        if ($recipientCount > $remainingToday) {
            return [
                'enabled' => true,
                'queued' => 0,
                'duplicates' => 0,
                'invalid' => $invalid,
                'audience' => $audience,
                'blocked' => true,
                'recipient_count' => $recipientCount,
                'message' => "WhatsApp was not queued: {$recipientCount} recipients need more than the {$remainingToday} daily slot(s) currently available.",
            ];
        }

        $legacySubject = $audience === 'invited'
            ? 'You are invited: {walk_title}'
            : 'New Pyrk walk: {walk_title}';
        $legacyMessage = $audience === 'invited'
            ? "Hello {first_name},\n\nYou are invited to join an upcoming Pyrk walk.\n\n🥾 {walk_title}\n📅 {walk_date}\n🕒 {walk_time}\n📍 {route_name}\n📏 {distance_km} KM\n\nPlease review the walk details, sign the waiver if required, and register here:\n{walk_link}\n\nSee you on the trail!\nPyrk Walking Club"
            : "Hello {first_name},\n\nA new Pyrk walk is now available.\n\n🥾 {walk_title}\n📅 {walk_date}\n🕒 {walk_time}\n📍 {route_name}\n📏 {distance_km} KM\n\nView the walk details, sign the waiver if required, and register here:\n{walk_link}\n\nSee you on the trail!\nPyrk Walking Club";
        $templates = pyrk_walk_whatsapp_resolve_templates($pdo, $walkInput, $legacySubject, $legacyMessage);
        if (empty($templates['ok'])) {
            return [
                'enabled' => true,
                'queued' => 0,
                'duplicates' => 0,
                'invalid' => $invalid,
                'audience' => $audience,
                'blocked' => true,
                'message' => (string)($templates['error'] ?? 'WhatsApp content is required.'),
            ];
        }
        $subjectTemplate = (string)$templates['subject'];
        $messageTemplate = (string)$templates['message'];
        $presetId = (int)($templates['preset_id'] ?? 0);

        $queued = 0;
        $duplicates = 0;
        $failed = 0;
        $errors = [];
        foreach ($uniqueRecipients as $recipient) {
            $subject = pyrk_walk_whatsapp_render($subjectTemplate, $walk, $recipient, $walkId);
            $messageText = pyrk_walk_whatsapp_render($messageTemplate, $walk, $recipient, $walkId);
            $body = '*' . str_replace('*', '', $subject) . "\n\n" . $messageText;

            $walkUrl = pyrk_walk_notification_walk_url($walkId);
            if ($walkUrl !== '' && !str_contains($body, $walkUrl)) {
                $body .= "\n\nView walk: " . $walkUrl;
            }
            $body = function_exists('mb_substr') ? mb_substr($body, 0, 4000, 'UTF-8') : substr($body, 0, 4000);

            $result = $queue->enqueue([
                'user_id' => (int)$recipient['user_id'],
                'walk_id' => $walkId,
                'mobile' => (string)$recipient['mobile'],
                'message' => $body,
                'created_by' => $adminId,
            ]);
            $status = (string)($result['status'] ?? 'failed');
            if ($status === 'queued') {
                $queued++;
            } elseif ($status === 'duplicate') {
                $duplicates++;
            } else {
                $failed++;
                if (!empty($result['error']) && count($errors) < 3) $errors[] = (string)$result['error'];
            }
        }

        try {
            Security::audit($adminId, 'walk_whatsapp_notification_queued', 'walks', $walkId, [
                'audience' => $audience,
                'requested_audience' => $requestedAudience,
                'queued' => $queued,
                'duplicates' => $duplicates,
                'invalid' => $invalid,
                'failed' => $failed,
                'preset_id' => $presetId,
            ]);
        } catch (Throwable) {}

        return [
            'enabled' => true,
            'queued' => $queued,
            'duplicates' => $duplicates,
            'invalid' => $invalid,
            'failed' => $failed,
            'audience' => $audience,
            'message' => $errors ? implode(' ', $errors) : null,
        ];
    } catch (Throwable $e) {
        return [
            'enabled' => true,
            'queued' => 0,
            'blocked' => true,
            'message' => 'WhatsApp could not be queued: ' . $e->getMessage(),
        ];
    }
}

function pyrk_walk_whatsapp_notification_message(array $result): string
{
    if (empty($result['enabled'])) return '';

    $queued = (int)($result['queued'] ?? 0);
    $duplicates = (int)($result['duplicates'] ?? 0);
    $invalid = (int)($result['invalid'] ?? 0);
    $failed = (int)($result['failed'] ?? 0);

    if ($queued > 0) {
        $message = " WhatsApp queued safely for {$queued} member(s); the cron sends them gradually.";
        if ($duplicates > 0) $message .= " {$duplicates} duplicate(s) were skipped.";
        if ($invalid > 0) $message .= " {$invalid} invalid/missing number(s) were skipped.";
        if ($failed > 0) $message .= " {$failed} could not be queued.";
        return $message;
    }

    $reason = trim((string)($result['message'] ?? ''));
    return $reason !== '' ? ' ' . $reason : ' WhatsApp was enabled, but no messages were queued.';
}


function pyrk_invite_notification_message(array $notifyResult, int $newInviteCount = 0): string
{
    if (($notifyResult['status'] ?? '') === 'disabled') {
        return $newInviteCount > 0
            ? " Invitations saved. {$newInviteCount} new invitee(s) were added, but invitation emails are disabled."
            : ' Invitations saved.';
    }
    $sent = (int)($notifyResult['sent'] ?? 0) + (int)($notifyResult['logged'] ?? 0);
    $failed = (int)($notifyResult['failed'] ?? 0);
    if ($sent > 0 && $failed > 0) {
        return " {$sent} invitation email(s) sent/logged. {$failed} failed — check Email Logs.";
    }
    if ($sent > 0) {
        return " {$sent} invitation email(s) sent/logged to newly invited member(s).";
    }
    if ($failed > 0) {
        return " New invitee(s) were added, but {$failed} email(s) failed — check Email Logs.";
    }
    return $newInviteCount > 0 ? ' New invitee(s) saved. No new email was sent.' : ' Invitations saved.';
}

if ($method === 'GET') {
    if ($action === 'detail' && !empty($input['id'])) {
        $stmt = $pdo->prepare('SELECT w.*, r.route_name, r.description AS route_description, r.start_address, r.end_address, r.total_km, r.google_polyline, r.map_bounds_json FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? AND ' . pyrk_walk_visible_sql($pdo, 'w') . ' LIMIT 1');
        $stmt->execute([(int)$input['id']]);
        $walk = $stmt->fetch();
        $points = [];
        if ($walk) {
            if (!pyrk_walk_user_can_access_walk($pdo, $walk, $currentUser)) {
                Response::error('You do not have access to this walk.', 403);
            }
            $pts = $pdo->prepare('SELECT rp.* FROM route_points rp WHERE rp.route_id = ? ORDER BY sort_order, id');
            $pts->execute([(int)$walk['route_id']]);
            $points = $pts->fetchAll();
        }
        $myRegistration = $walk ? pyrk_user_registration($pdo, (int)$walk['id'], $currentUser) : null;
        if ($walk) {
            $walk['my_registration_status'] = $myRegistration['registration_status'] ?? null;
            $walk['my_attendance_status'] = $myRegistration['attendance_status'] ?? null;
            $walk['my_check_in_time'] = $myRegistration['check_in_time'] ?? null;
            $walk['my_finish_time'] = $myRegistration['finish_time'] ?? null;
            $walk['my_verified_km'] = $myRegistration['verified_km'] ?? null;
            $walk['is_registered'] = $myRegistration ? 1 : 0;
            $walk['my_waiver_signed'] = pyrk_user_waiver_signed($pdo, (int)$walk['id'], $currentUser) ? 1 : 0;
            $walk['is_finished_by_me'] = pyrk_member_walk_is_finished($myRegistration) ? 1 : 0;
            $walk['is_partially_completed_by_me'] = pyrk_member_walk_is_partial($myRegistration) ? 1 : 0;
        }
        Response::ok(['walk' => $walk, 'points' => $points, 'my_registration' => $myRegistration]);
    }

    if ($action === 'registrants' && !empty($input['id'])) {
        $viewer = Auth::requireUser();
        $walkId = (int)$input['id'];
        pyrk_can_view_walk_registrants($viewer, $walkId, $pdo);
        $walkStmt = $pdo->prepare('SELECT w.*, r.route_name, r.start_address, r.end_address, r.total_km FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? AND ' . pyrk_walk_visible_sql($pdo, 'w') . ' LIMIT 1');
        $walkStmt->execute([$walkId]);
        $walk = $walkStmt->fetch();
        if (!$walk) Response::error('Walk not found.', 404);

        $registrants = $pdo->prepare('SELECT wr.id AS registration_id, wr.registration_status, wr.registered_at, u.id AS user_id, u.full_name, u.email, u.mobile, u.profile_photo_path, mp.emergency_contact_name, mp.emergency_contact_mobile, mp.medical_conditions, a.status AS attendance_status, a.check_in_time, a.finish_time, a.verified_km, CASE WHEN ws.id IS NULL THEN 0 ELSE 1 END AS waiver_signed FROM walk_registrations wr JOIN users u ON u.id = wr.user_id LEFT JOIN member_profiles mp ON mp.user_id = u.id LEFT JOIN attendance a ON a.walk_id = wr.walk_id AND a.user_id = wr.user_id LEFT JOIN waiver_signatures ws ON ws.walk_id = wr.walk_id AND ws.user_id = wr.user_id WHERE wr.walk_id = ? AND wr.registration_status <> "cancelled" ORDER BY wr.registered_at DESC, u.full_name ASC');
        $registrants->execute([$walkId]);
        Response::ok(['walk' => $walk, 'registrants' => $registrants->fetchAll(), 'invitations' => pyrk_invitation_summary($pdo, $walkId)]);
    }

    if ($action === 'invitations' && !empty($input['id'])) {
        $viewer = Auth::requireUser();
        $walkId = (int)$input['id'];
        if (!pyrk_walk_user_has_own_walk_full_access($pdo, $walkId, $viewer)) {
            pyrk_can_open_walk_permissions($viewer);
        }
        $walkStmt = $pdo->prepare('SELECT w.*, r.route_name, r.total_km FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? LIMIT 1');
        $walkStmt->execute([$walkId]);
        $walk = $walkStmt->fetch();
        if (!$walk) Response::error('Walk not found.', 404);
        $members = $pdo->query('SELECT u.id, u.full_name, u.email, u.mobile, u.profile_photo_path, COALESCE(mp.total_walks,0) AS total_walks, COALESCE(mp.total_kilometers,0) AS total_kilometers, COALESCE(mp.loyalty_points,0) AS loyalty_points FROM users u JOIN roles r ON r.id = u.role_id LEFT JOIN member_profiles mp ON mp.user_id = u.id WHERE r.name = "member" AND u.status = "active" ORDER BY u.full_name ASC')->fetchAll();
        $groups = $pdo->query('SELECT g.*, (SELECT COUNT(*) FROM member_group_members mgm WHERE mgm.group_id = g.id) AS member_count FROM member_groups g WHERE g.status = "active" ORDER BY g.group_name ASC')->fetchAll();

        $membershipRows = $pdo->query('SELECT mgm.group_id, mgm.user_id FROM member_group_members mgm JOIN users u ON u.id = mgm.user_id JOIN roles r ON r.id = u.role_id JOIN member_groups g ON g.id = mgm.group_id WHERE r.name = "member" AND u.status = "active" AND g.status = "active" ORDER BY mgm.group_id, u.full_name')->fetchAll();
        $groupMembers = [];
        $memberGroups = [];
        foreach ($membershipRows as $row) {
            $groupId = (int)$row['group_id'];
            $userId = (int)$row['user_id'];
            if (!isset($groupMembers[$groupId])) $groupMembers[$groupId] = [];
            if (!isset($memberGroups[$userId])) $memberGroups[$userId] = [];
            $groupMembers[$groupId][] = $userId;
            $memberGroups[$userId][] = $groupId;
        }
        foreach ($groups as &$group) {
            $gid = (int)$group['id'];
            $group['member_ids'] = $groupMembers[$gid] ?? [];
        }
        unset($group);
        foreach ($members as &$member) {
            $mid = (int)$member['id'];
            $member['group_ids'] = $memberGroups[$mid] ?? [];
        }
        unset($member);

        Response::ok(['walk' => $walk, 'members' => $members, 'groups' => $groups, 'group_members' => $groupMembers, 'member_groups' => $memberGroups, 'invitations' => pyrk_invitation_summary($pdo, $walkId)]);
    }


    if ($action === 'team_options' && !empty($input['id'])) {
        $viewer = Auth::requireUser();
        $walkId = (int)$input['id'];

        $walkStmt = $pdo->prepare('SELECT w.*, r.route_name FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? AND ' . pyrk_walk_visible_sql($pdo, 'w') . ' LIMIT 1');
        $walkStmt->execute([$walkId]);
        $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
        if (!$walk) Response::error('Walk not found.', 404);

        if (!pyrk_walk_user_can_view_all_walks($viewer) && !pyrk_walk_user_is_assigned($pdo, $walkId, $viewer) && !pyrk_walk_user_is_creator($pdo, $walkId, $viewer)) {
            Response::error('You can only view the team for walks created by you or assigned to you.', 403);
        }

        if (!pyrk_walk_team_table_exists($pdo)) {
            Response::error('Walk team assignments are not installed yet. Please run database/migrations/034_walk_team_assignments.sql', 500);
        }

        $canManageTeam = pyrk_walk_user_can_manage_team($pdo, $walkId, $viewer);
        Response::ok([
            'walk' => $walk,
            // Only users who can assign staff should receive the full staff directory.
            'users' => $canManageTeam ? pyrk_walk_staff_options($pdo) : [],
            'assignments' => pyrk_walk_team_assignments($pdo, $walkId),
            'can_manage_team' => $canManageTeam,
            'tables_ready' => true,
        ]);
    }

    $mode = (string)$action;
    if ($mode === 'archived') {
        $viewer = Auth::requireUser();
        if (!pyrk_walk_user_can_view_all_walks($viewer) && !Auth::hasPermission('walks.edit', $viewer) && !Auth::hasPermission('walks.manage', $viewer) && !Auth::hasPermission('walks.delete', $viewer)) {
            Response::error('You do not have permission to view archived walks.', 403);
        }
        $filter = pyrk_walk_archive_where_sql($pdo, 'w');
    } elseif ($mode === 'admin_open') {
        // Operational Open section: date never closes a walk automatically.
        // An upcoming Open walk intentionally appears in both Upcoming and Open.
        $filter = 'LOWER(TRIM(COALESCE(w.status, ""))) = "open" AND ' . pyrk_walk_visible_sql($pdo, 'w');
    } elseif ($mode === 'admin_closed') {
        // Closed section is explicit and status-based only.
        $filter = 'LOWER(TRIM(COALESCE(w.status, ""))) IN ("closed", "completed") AND ' . pyrk_walk_visible_sql($pdo, 'w');
    } elseif (in_array($mode, ['admin_finished', 'finished', 'completed'], true)) {
        // Legacy compatibility for old links/API clients. The new UI uses admin_closed.
        $filter = 'LOWER(TRIM(COALESCE(w.status, ""))) IN ("closed", "completed") AND ' . pyrk_walk_visible_sql($pdo, 'w');
    } elseif (in_array($mode, ['admin_upcoming', 'upcoming'], true)) {
        // Upcoming is date-based. Open future walks intentionally also appear in Open.
        $filter = 'w.walk_date >= CURDATE() AND LOWER(TRIM(COALESCE(w.status, ""))) NOT IN ("closed", "completed") AND ' . pyrk_walk_visible_sql($pdo, 'w');
    } elseif (in_array($mode, ['all', 'admin_all', 'list_all'], true)) {
        // Kept for backward compatibility. New admin UI uses admin_upcoming/admin_open/admin_closed/archived.
        $filter = '1=1 AND ' . pyrk_walk_visible_sql($pdo, 'w');
    } else {
        $filter = ($mode === 'past' ? 'w.walk_date < CURDATE()' : 'w.walk_date >= CURDATE()') . ' AND ' . pyrk_walk_visible_sql($pdo, 'w');
    }

    // The public/member list must not expose admin drafts. Admin sections use
    // dedicated admin actions and continue to show drafts only where appropriate.
    if ($mode === 'list' && (!$currentUser || (($currentUser['role_name'] ?? '') === 'member'))) {
        $filter .= ' AND LOWER(TRIM(COALESCE(w.status, ""))) <> "draft"';
    }

    $params = [];
    $accessSql = '';
    $teamAccessTableReady = pyrk_walk_team_table_exists($pdo);

    if (!pyrk_walk_user_can_view_all_walks($currentUser)) {
        if ($currentUser && (($currentUser['role_name'] ?? '') !== 'member')) {
            // Staff users see only walks they created or where they are assigned.
            // They do NOT see unrelated open walks.
            // If the team assignment migration is missing, keep the page working with created-by access only.
            if ($teamAccessTableReady) {
                $accessSql = ' AND (w.created_by = ? OR EXISTS (SELECT 1 FROM walk_staff_assignments wsa_access WHERE wsa_access.walk_id = w.id AND wsa_access.user_id = ?) OR EXISTS (SELECT 1 FROM walk_registrations wr_access WHERE wr_access.walk_id = w.id AND wr_access.user_id = ? AND wr_access.registration_status IN ("registered","approved")) OR EXISTS (SELECT 1 FROM attendance a_access WHERE a_access.walk_id = w.id AND a_access.user_id = ? AND (a_access.check_in_time IS NOT NULL OR a_access.status IN ("checked_in","started","finished","partially_completed","manual_verified"))))';
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
            } else {
                $accessSql = ' AND (w.created_by = ? OR EXISTS (SELECT 1 FROM walk_registrations wr_access WHERE wr_access.walk_id = w.id AND wr_access.user_id = ? AND wr_access.registration_status IN ("registered","approved")) OR EXISTS (SELECT 1 FROM attendance a_access WHERE a_access.walk_id = w.id AND a_access.user_id = ? AND (a_access.check_in_time IS NOT NULL OR a_access.status IN ("checked_in","started","finished","partially_completed","manual_verified"))))';
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
            }
        } elseif ($currentUser) {
            // Members keep normal client access: open walks, own rows, optional staff assignment, or invitations.
            if ($teamAccessTableReady) {
                $accessSql = ' AND (w.access_type = "open" OR w.created_by = ? OR EXISTS (SELECT 1 FROM walk_staff_assignments wsa_access WHERE wsa_access.walk_id = w.id AND wsa_access.user_id = ?) OR EXISTS (SELECT 1 FROM walk_registrations wr_access WHERE wr_access.walk_id = w.id AND wr_access.user_id = ? AND wr_access.registration_status IN ("registered","approved")) OR EXISTS (SELECT 1 FROM attendance a_access WHERE a_access.walk_id = w.id AND a_access.user_id = ? AND (a_access.check_in_time IS NOT NULL OR a_access.status IN ("checked_in","started","finished","partially_completed","manual_verified"))) OR EXISTS (SELECT 1 FROM walk_invitations wi WHERE wi.walk_id = w.id AND wi.status = "invited" AND (wi.user_id = ? OR wi.group_id IN (SELECT mgm.group_id FROM member_group_members mgm WHERE mgm.user_id = ?))))';
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
            } else {
                $accessSql = ' AND (w.access_type = "open" OR w.created_by = ? OR EXISTS (SELECT 1 FROM walk_registrations wr_access WHERE wr_access.walk_id = w.id AND wr_access.user_id = ? AND wr_access.registration_status IN ("registered","approved")) OR EXISTS (SELECT 1 FROM attendance a_access WHERE a_access.walk_id = w.id AND a_access.user_id = ? AND (a_access.check_in_time IS NOT NULL OR a_access.status IN ("checked_in","started","finished","partially_completed","manual_verified"))) OR EXISTS (SELECT 1 FROM walk_invitations wi WHERE wi.walk_id = w.id AND wi.status = "invited" AND (wi.user_id = ? OR wi.group_id IN (SELECT mgm.group_id FROM member_group_members mgm WHERE mgm.user_id = ?))))';
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
                $params[] = (int)$currentUser['id'];
            }
        } else {
            $accessSql = ' AND w.access_type = "open"';
        }
    }
    $selectParams = [];
    $myRegistrationSql = ', NULL AS my_registration_status, NULL AS my_attendance_status, NULL AS my_check_in_time, NULL AS my_finish_time, NULL AS my_verified_km, 0 AS is_registered, 0 AS is_finished_by_me, 0 AS is_partially_completed_by_me';
    if ($currentUser) {
        $myRegistrationSql = ', (SELECT wr.registration_status FROM walk_registrations wr WHERE wr.walk_id = w.id AND wr.user_id = ? AND wr.registration_status IN (\'registered\',\'approved\') LIMIT 1) AS my_registration_status, (SELECT a.status FROM attendance a WHERE a.walk_id = w.id AND a.user_id = ? LIMIT 1) AS my_attendance_status, (SELECT a.check_in_time FROM attendance a WHERE a.walk_id = w.id AND a.user_id = ? LIMIT 1) AS my_check_in_time, (SELECT a.finish_time FROM attendance a WHERE a.walk_id = w.id AND a.user_id = ? LIMIT 1) AS my_finish_time, (SELECT a.verified_km FROM attendance a WHERE a.walk_id = w.id AND a.user_id = ? LIMIT 1) AS my_verified_km, EXISTS(SELECT 1 FROM walk_registrations wr2 WHERE wr2.walk_id = w.id AND wr2.user_id = ? AND wr2.registration_status IN (\'registered\',\'approved\')) AS is_registered, EXISTS(SELECT 1 FROM attendance af WHERE af.walk_id = w.id AND af.user_id = ? AND (af.status IN (\'finished\', \'manual_verified\') OR (af.finish_time IS NOT NULL AND af.status <> \'partially_completed\'))) AS is_finished_by_me, EXISTS(SELECT 1 FROM attendance ap WHERE ap.walk_id = w.id AND ap.user_id = ? AND ap.status = \'partially_completed\') AS is_partially_completed_by_me';
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
        $selectParams[] = (int)$currentUser['id'];
    }
    $teamSql = pyrk_walk_team_table_exists($pdo)
        ? ", (SELECT GROUP_CONCAT(u.full_name ORDER BY u.full_name SEPARATOR ', ') FROM walk_staff_assignments wsa JOIN users u ON u.id = wsa.user_id WHERE wsa.walk_id = w.id AND wsa.assignment_role = 'manager') AS manager_names,
             (SELECT GROUP_CONCAT(u.full_name ORDER BY u.full_name SEPARATOR ', ') FROM walk_staff_assignments wsa JOIN users u ON u.id = wsa.user_id WHERE wsa.walk_id = w.id AND wsa.assignment_role = 'head_walker') AS head_walker_names,
             (SELECT GROUP_CONCAT(u.full_name ORDER BY u.full_name SEPARATOR ', ') FROM walk_staff_assignments wsa JOIN users u ON u.id = wsa.user_id WHERE wsa.walk_id = w.id AND wsa.assignment_role = 'walker') AS assigned_walker_names,
             (SELECT COUNT(*) FROM walk_staff_assignments wsa WHERE wsa.walk_id = w.id AND wsa.assignment_role = 'walker') AS assigned_walker_count"
        : ", NULL AS manager_names, NULL AS head_walker_names, NULL AS assigned_walker_names, 0 AS assigned_walker_count";

    if ($mode === 'admin_open') {
        // Recently due/overdue open walks first, followed by the nearest upcoming open walks.
        $orderSql = 'ORDER BY CASE WHEN w.walk_date < CURDATE() THEN 0 ELSE 1 END, CASE WHEN w.walk_date < CURDATE() THEN w.walk_date END DESC, CASE WHEN w.walk_date >= CURDATE() THEN w.walk_date END ASC, w.start_time ASC';
    } else {
        $orderSql = in_array($mode, ['all', 'admin_all', 'list_all', 'admin_closed', 'admin_finished', 'finished', 'completed', 'archived'], true)
            ? 'ORDER BY w.walk_date DESC, w.start_time DESC'
            : 'ORDER BY w.walk_date ASC, w.start_time ASC';
    }

    $sql = "SELECT w.*, r.route_name, r.description AS route_description, r.start_address, r.end_address, r.total_km, (SELECT COUNT(*) FROM walk_registrations wr WHERE wr.walk_id = w.id AND wr.registration_status IN ('registered','approved')) AS registered_count, (SELECT COUNT(*) FROM attendance a WHERE a.walk_id = w.id AND a.check_in_time IS NOT NULL) AS checked_in_count, (SELECT COUNT(*) FROM attendance a WHERE a.walk_id = w.id AND (a.status = 'finished' OR a.finish_time IS NOT NULL)) AS finished_count, (SELECT COUNT(*) FROM walk_invitations wi WHERE wi.walk_id = w.id AND wi.user_id IS NOT NULL) AS invited_member_count, (SELECT COUNT(*) FROM walk_invitations wi WHERE wi.walk_id = w.id AND wi.group_id IS NOT NULL) AS invited_group_count {$teamSql} {$myRegistrationSql} FROM walks w JOIN routes r ON r.id = w.route_id WHERE {$filter}{$accessSql} {$orderSql}";
    $stmt = $pdo->prepare($sql);
    $stmt->execute(array_merge($selectParams, $params));
    Response::ok(['walks' => $stmt->fetchAll()]);
}


if ($method === 'POST' && $action === 'save_team') {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();

    if (!pyrk_walk_team_table_exists($pdo)) {
        Response::error('Walk team table is not installed. Run database/migrations/034_walk_team_assignments.sql');
    }

    $walkId = (int)($input['walk_id'] ?? $input['id'] ?? 0);
    if ($walkId <= 0) Response::error('Missing walk id.');

    if (!pyrk_walk_is_visible($pdo, $walkId)) {
        Response::error('Walk not found or it was deleted.', 404);
    }

    $walkStmt = $pdo->prepare('SELECT id, title FROM walks WHERE id = ? LIMIT 1');
    $walkStmt->execute([$walkId]);
    $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
    if (!$walk) Response::error('Walk not found.', 404);

    if (!pyrk_walk_user_can_manage_team($pdo, $walkId, $admin)) {
        Response::error('Only admins or the assigned walk manager can change this walk team.', 403);
    }

    $managerIds = pyrk_walk_staff_ids_from_input($input['manager_ids'] ?? ($input['manager_id'] ?? []));
    $headWalkerIds = pyrk_walk_staff_ids_from_input($input['head_walker_ids'] ?? ($input['head_walker_id'] ?? []));
    $walkerIds = pyrk_walk_staff_ids_from_input($input['walker_ids'] ?? []);

    $pdo->beginTransaction();
    try {
        pyrk_walk_sync_staff_assignments($pdo, $walkId, $managerIds, $headWalkerIds, $walkerIds, (int)$admin['id']);
        $pdo->commit();

        Security::audit((int)$admin['id'], 'walk_team_updated', 'walks', $walkId, [
            'manager_count' => count($managerIds),
            'head_walker_count' => count($headWalkerIds),
            'walker_count' => count($walkerIds),
        ]);

        Response::ok([
            'assignments' => pyrk_walk_team_assignments($pdo, $walkId),
        ], 'Walk team saved.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not save walk team: ' . $e->getMessage());
    }
}


if ($method === 'POST' && $action === 'duplicate') {
    $admin = Auth::requireUser();
    pyrk_can_create_walks($admin);
    Csrf::validateFromRequest();

    $sourceWalkId = (int)($input['id'] ?? $input['source_walk_id'] ?? 0);
    if ($sourceWalkId <= 0) Response::error('Missing source walk id.');

    $sourceStmt = $pdo->prepare('SELECT w.*, r.route_name, r.total_km FROM walks w JOIN routes r ON r.id = w.route_id WHERE w.id = ? LIMIT 1');
    $sourceStmt->execute([$sourceWalkId]);
    $sourceWalk = $sourceStmt->fetch(PDO::FETCH_ASSOC);
    if (!$sourceWalk) Response::error('Source walk not found.', 404);

    // A user may duplicate only a walk they fully manage: global full access,
    // the creator, or the assigned walk manager.
    if (!pyrk_walk_user_has_own_walk_full_access($pdo, $sourceWalkId, $admin)) {
        Response::error('You can duplicate only walks you created, walks where you are the assigned manager, or walks available through full walk access.', 403);
    }

    $title = trim((string)($input['title'] ?? ''));
    if ($title === '') {
        $title = trim((string)($sourceWalk['title'] ?? 'Walk')) . ' - Copy';
    }
    if ((function_exists('mb_strlen') ? mb_strlen($title) : strlen($title)) > 180) Response::error('Walk title cannot exceed 180 characters.');

    $walkDate = trim((string)($input['walk_date'] ?? ''));
    $startTime = substr(trim((string)($input['start_time'] ?? $sourceWalk['start_time'] ?? '')), 0, 5);
    $finishTime = substr(trim((string)($input['expected_finish_time'] ?? $sourceWalk['expected_finish_time'] ?? '')), 0, 5);
    if ($walkDate === '' || $startTime === '' || $finishTime === '') {
        Response::error('New walk date, start time, and expected finish time are required.');
    }

    $dateObject = DateTimeImmutable::createFromFormat('!Y-m-d', $walkDate);
    $dateErrors = DateTimeImmutable::getLastErrors();
    if (!$dateObject || (is_array($dateErrors) && (($dateErrors['warning_count'] ?? 0) > 0 || ($dateErrors['error_count'] ?? 0) > 0))) {
        Response::error('Invalid new walk date.');
    }
    if ($walkDate < date('Y-m-d')) {
        Response::error('The duplicated walk must be scheduled for today or a future date.');
    }

    $status = strtolower(trim((string)($input['status'] ?? 'draft')));
    if (!in_array($status, ['draft', 'open'], true)) $status = 'draft';

    $copyDeadlines = !empty($input['copy_deadlines']);
    $copyTeam = !empty($input['copy_team']);
    $copyInvitations = !empty($input['copy_invitations']);

    $registrationDeadline = $copyDeadlines
        ? pyrk_walk_shift_relative_datetime(
            $sourceWalk['registration_deadline'] ?? null,
            (string)$sourceWalk['walk_date'],
            (string)$sourceWalk['start_time'],
            $walkDate,
            $startTime
        )
        : null;
    $votingDeadline = $copyDeadlines
        ? pyrk_walk_shift_relative_datetime(
            $sourceWalk['voting_deadline'] ?? null,
            (string)$sourceWalk['walk_date'],
            (string)$sourceWalk['start_time'],
            $walkDate,
            $startTime
        )
        : null;

    $validationInput = [
        'walk_date' => $walkDate,
        'start_time' => $startTime,
        'expected_finish_time' => $finishTime,
        'registration_deadline' => $registrationDeadline,
        'voting_deadline' => $votingDeadline,
    ];
    pyrk_validate_walk_schedule($validationInput);

    $payment = pyrk_walk_payment_input([
        'payment_model' => $sourceWalk['payment_model'] ?? 'free',
        'payment_amount' => $sourceWalk['payment_amount'] ?? 0,
        'payment_currency' => $sourceWalk['payment_currency'] ?? 'LBP',
    ]);

    $memberIds = [];
    $groupIds = [];
    if ($copyInvitations) {
        $memberStmt = $pdo->prepare('SELECT DISTINCT wi.user_id FROM walk_invitations wi JOIN users u ON u.id = wi.user_id JOIN roles r ON r.id = u.role_id WHERE wi.walk_id = ? AND wi.invitation_type = "member" AND wi.status = "invited" AND wi.user_id IS NOT NULL AND u.status = "active" AND r.name = "member"');
        $memberStmt->execute([$sourceWalkId]);
        $memberIds = array_values(array_unique(array_map('intval', $memberStmt->fetchAll(PDO::FETCH_COLUMN))));

        $groupStmt = $pdo->prepare('SELECT DISTINCT wi.group_id FROM walk_invitations wi JOIN member_groups g ON g.id = wi.group_id WHERE wi.walk_id = ? AND wi.invitation_type = "group" AND wi.status = "invited" AND wi.group_id IS NOT NULL AND g.status = "active"');
        $groupStmt->execute([$sourceWalkId]);
        $groupIds = array_values(array_unique(array_map('intval', $groupStmt->fetchAll(PDO::FETCH_COLUMN))));
    }

    $teamAssignments = $copyTeam && pyrk_walk_team_table_exists($pdo)
        ? pyrk_walk_team_assignments($pdo, $sourceWalkId)
        : ['manager' => [], 'head_walker' => [], 'walker' => []];
    $managerIds = array_values(array_filter(array_map(static fn($row) => (int)($row['user_id'] ?? 0), $teamAssignments['manager'] ?? [])));
    $headWalkerIds = array_values(array_filter(array_map(static fn($row) => (int)($row['user_id'] ?? 0), $teamAssignments['head_walker'] ?? [])));
    $walkerIds = array_values(array_filter(array_map(static fn($row) => (int)($row['user_id'] ?? 0), $teamAssignments['walker'] ?? [])));

    // Do not carry inactive/deactivated accounts into a new operational team.
    $allTeamIds = array_values(array_unique(array_merge($managerIds, $headWalkerIds, $walkerIds)));
    if ($allTeamIds) {
        $placeholders = implode(',', array_fill(0, count($allTeamIds), '?'));
        $activeTeamStmt = $pdo->prepare("SELECT id FROM users WHERE status = 'active' AND id IN ({$placeholders})");
        $activeTeamStmt->execute($allTeamIds);
        $activeTeamMap = array_flip(array_map('intval', $activeTeamStmt->fetchAll(PDO::FETCH_COLUMN)));
        $managerIds = array_values(array_filter($managerIds, static fn($id) => isset($activeTeamMap[(int)$id])));
        $headWalkerIds = array_values(array_filter($headWalkerIds, static fn($id) => isset($activeTeamMap[(int)$id])));
        $walkerIds = array_values(array_filter($walkerIds, static fn($id) => isset($activeTeamMap[(int)$id])));
    }

    $pdo->beginTransaction();
    try {
        $insert = $pdo->prepare('INSERT INTO walks (route_id, title, description, walk_date, start_time, expected_finish_time, registration_deadline, capacity, status, access_type, invite_note, waiver_required, allow_self_checkin, payment_model, payment_amount, payment_currency, voting_status, voting_deadline, checkin_token, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "open", ?, ?, ?, NOW(), NOW())');
        $token = 'PYRK-WALK-' . strtoupper(bin2hex(random_bytes(8)));
        $insert->execute([
            (int)$sourceWalk['route_id'],
            $title,
            trim((string)($sourceWalk['description'] ?? '')),
            $walkDate,
            $startTime,
            $finishTime,
            $registrationDeadline,
            (int)($sourceWalk['capacity'] ?? 0),
            $status,
            in_array(($sourceWalk['access_type'] ?? 'open'), ['open', 'invitation_only'], true) ? $sourceWalk['access_type'] : 'open',
            trim((string)($sourceWalk['invite_note'] ?? '')),
            !empty($sourceWalk['waiver_required']) ? 1 : 0,
            !empty($sourceWalk['allow_self_checkin']) ? 1 : 0,
            $payment['payment_model'],
            $payment['payment_amount'],
            $payment['payment_currency'],
            $votingDeadline,
            $token,
            (int)$admin['id'],
        ]);

        $duplicateWalkId = (int)$pdo->lastInsertId();
        $inviteSync = pyrk_sync_walk_invitations($pdo, $duplicateWalkId, $memberIds, $groupIds, (int)$admin['id']);

        if ($copyTeam && pyrk_walk_team_table_exists($pdo)) {
            pyrk_walk_sync_staff_assignments($pdo, $duplicateWalkId, $managerIds, $headWalkerIds, $walkerIds, (int)$admin['id']);
        }

        Security::audit((int)$admin['id'], 'walk_duplicated', 'walks', $duplicateWalkId, [
            'source_walk_id' => $sourceWalkId,
            'source_walk_title' => $sourceWalk['title'] ?? null,
            'title' => $title,
            'walk_date' => $walkDate,
            'status' => $status,
            'copy_deadlines' => $copyDeadlines ? 1 : 0,
            'copy_team' => $copyTeam ? 1 : 0,
            'copy_invitations' => $copyInvitations ? 1 : 0,
            'team_count' => count($managerIds) + count($headWalkerIds) + count($walkerIds),
            'member_invitation_count' => count($memberIds),
            'group_invitation_count' => count($groupIds),
        ]);

        $pdo->commit();
        Response::ok([
            'walk_id' => $duplicateWalkId,
            'source_walk_id' => $sourceWalkId,
            'title' => $title,
            'walk_date' => $walkDate,
            'status' => $status,
            'route_name' => $sourceWalk['route_name'] ?? '',
            'team_count' => count($managerIds) + count($headWalkerIds) + count($walkerIds),
            'member_invitation_count' => count($memberIds),
            'group_invitation_count' => count($groupIds),
            'invite_sync' => $inviteSync,
        ], 'Walk duplicated safely. Registrations, attendance, payments, check-ins, waivers, photos, and voting results were not copied. No email or member notification was sent.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not duplicate walk: ' . $e->getMessage());
    }
}


if ($method === 'POST' && $action === 'create') {
    $admin = Auth::requireUser();
    pyrk_can_create_walks($admin);
    Csrf::validateFromRequest();
    foreach (['route_id','title','walk_date','start_time','expected_finish_time'] as $field) {
        if (empty($input[$field])) Response::error("Missing field: {$field}");
    }
    pyrk_validate_walk_schedule($input);
    $registrationDeadline = !empty($input['registration_deadline']) ? str_replace('T', ' ', (string)$input['registration_deadline']) : null;
    $votingDeadline = !empty($input['voting_deadline']) ? str_replace('T', ' ', (string)$input['voting_deadline']) : null;
    $accessType = in_array(($input['access_type'] ?? 'open'), ['open','invitation_only'], true) ? $input['access_type'] : 'open';
    $payment = pyrk_walk_payment_input($input);
    $memberIds = $input['invite_member_ids'] ?? [];
    $groupIds = $input['invite_group_ids'] ?? [];
    if (!is_array($memberIds)) $memberIds = array_filter(array_map('intval', explode(',', (string)$memberIds)));
    if (!is_array($groupIds)) $groupIds = array_filter(array_map('intval', explode(',', (string)$groupIds)));

    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('INSERT INTO walks (route_id, title, description, walk_date, start_time, expected_finish_time, registration_deadline, capacity, status, access_type, invite_note, waiver_required, allow_self_checkin, payment_model, payment_amount, payment_currency, voting_status, voting_deadline, checkin_token, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "open", ?, ?, ?, NOW(), NOW())');
        $token = 'PYRK-WALK-' . strtoupper(bin2hex(random_bytes(8)));
        $stmt->execute([(int)$input['route_id'], $input['title'], $input['description'] ?? '', $input['walk_date'], $input['start_time'], $input['expected_finish_time'], $registrationDeadline, (int)($input['capacity'] ?? 0), $input['status'] ?? 'open', $accessType, trim((string)($input['invite_note'] ?? '')), !empty($input['waiver_required']) ? 1 : 0, !empty($input['allow_self_checkin']) ? 1 : 0, $payment['payment_model'], $payment['payment_amount'], $payment['payment_currency'], $votingDeadline, $token, (int)$admin['id']]);
        $walkId = (int)$pdo->lastInsertId();
        $inviteSync = pyrk_sync_walk_invitations($pdo, $walkId, $memberIds, $groupIds, (int)$admin['id']);
        $pdo->commit();
        Security::audit((int)$admin['id'], 'walk_created', 'walks', $walkId, ['access_type' => $accessType, 'allow_self_checkin' => !empty($input['allow_self_checkin']) ? 1 : 0, 'payment_model' => $payment['payment_model'], 'payment_amount' => $payment['payment_amount'], 'payment_currency' => $payment['payment_currency'], 'members' => count($memberIds), 'groups' => count($groupIds)]);
        $notifier = new NotificationService();
        $notifier->notifyWalkCreated($walkId, (int)$admin['id']);
        $notifyResult = ['sent' => 0, 'logged' => 0, 'failed' => 0, 'skipped' => 0];
        if (!empty($input['notify_new_invitees']) && ($memberIds || $groupIds)) {
            $notifyResult = $notifier->notifyWalkInvitations($walkId, (int)$admin['id']);
        }
        $generalNotifyResult = pyrk_send_walk_general_notification($pdo, $walkId, $input, (int)$admin['id']);
        $whatsAppNotifyResult = pyrk_queue_walk_whatsapp_notification($pdo, $walkId, $input, (int)$admin['id']);
        Response::ok([
            'walk_id' => $walkId,
            'invite_sync' => $inviteSync,
            'invite_notifications' => $notifyResult,
            'general_notification' => $generalNotifyResult,
            'whatsapp_notification' => $whatsAppNotifyResult,
        ], 'Walk scheduled.' . pyrk_invite_notification_message($notifyResult, count($memberIds) + count($groupIds)) . pyrk_walk_general_notification_message($generalNotifyResult) . pyrk_walk_whatsapp_notification_message($whatsAppNotifyResult));
    } catch (Throwable $e) {
        $pdo->rollBack();
        Response::error('Could not create walk: ' . $e->getMessage());
    }
}


if ($method === 'POST' && $action === 'update') {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();

    $walkId = (int)($input['id'] ?? $input['walk_id'] ?? 0);
    if (!$walkId) Response::error('Missing walk id.');
    // Full access to other walks is explicit only (walks.manage / walks.view_all).
    // Creator or assigned Manager has full access only to this walk.
    if (!pyrk_walk_user_has_own_walk_full_access($pdo, $walkId, $admin)) {
        Response::error('You can edit only walks you created or walks where you are the assigned manager.', 403);
    }
    foreach (['route_id','title','walk_date','start_time','expected_finish_time'] as $field) {
        if (empty($input[$field])) Response::error("Missing field: {$field}");
    }
    pyrk_validate_walk_schedule($input);

    if (!pyrk_walk_is_visible($pdo, $walkId)) {
        Response::error('Walk not found or it was deleted.', 404);
    }

    $existing = $pdo->prepare('SELECT * FROM walks WHERE id = ? LIMIT 1');
    $existing->execute([$walkId]);
    $before = $existing->fetch();
    if (!$before) Response::error('Walk not found.', 404);

    $registrationDeadline = !empty($input['registration_deadline']) ? str_replace('T', ' ', (string)$input['registration_deadline']) : null;
    $votingDeadline = !empty($input['voting_deadline']) ? str_replace('T', ' ', (string)$input['voting_deadline']) : null;
    $accessType = in_array(($input['access_type'] ?? 'open'), ['open','invitation_only'], true) ? $input['access_type'] : 'open';
    $payment = pyrk_walk_payment_input($input);
    $memberIds = $input['invite_member_ids'] ?? [];
    $groupIds = $input['invite_group_ids'] ?? [];
    if (!is_array($memberIds)) $memberIds = array_filter(array_map('intval', explode(',', (string)$memberIds)));
    if (!is_array($groupIds)) $groupIds = array_filter(array_map('intval', explode(',', (string)$groupIds)));

    $pdo->beginTransaction();
    try {
        $stmt = $pdo->prepare('UPDATE walks SET route_id = ?, title = ?, description = ?, walk_date = ?, start_time = ?, expected_finish_time = ?, registration_deadline = ?, capacity = ?, status = ?, access_type = ?, invite_note = ?, waiver_required = ?, allow_self_checkin = ?, payment_model = ?, payment_amount = ?, payment_currency = ?, voting_deadline = ?, updated_at = NOW() WHERE id = ?');
        $stmt->execute([
            (int)$input['route_id'],
            trim((string)$input['title']),
            trim((string)($input['description'] ?? '')),
            $input['walk_date'],
            $input['start_time'],
            $input['expected_finish_time'],
            $registrationDeadline,
            (int)($input['capacity'] ?? 0),
            $input['status'] ?? 'open',
            $accessType,
            trim((string)($input['invite_note'] ?? '')),
            !empty($input['waiver_required']) ? 1 : 0,
            !empty($input['allow_self_checkin']) ? 1 : 0,
            $payment['payment_model'],
            $payment['payment_amount'],
            $payment['payment_currency'],
            $votingDeadline,
            $walkId,
        ]);
        $inviteSync = pyrk_sync_walk_invitations($pdo, $walkId, $memberIds, $groupIds, (int)$admin['id']);
        $pdo->commit();

        Security::audit((int)$admin['id'], 'walk_updated', 'walks', $walkId, [
            'access_type' => $accessType,
            'allow_self_checkin' => !empty($input['allow_self_checkin']) ? 1 : 0,
            'payment_model' => $payment['payment_model'],
            'payment_amount' => $payment['payment_amount'],
            'payment_currency' => $payment['payment_currency'],
            'members' => count($memberIds),
            'groups' => count($groupIds),
        ], $before);

        $notifyResult = ['sent' => 0, 'logged' => 0, 'failed' => 0, 'skipped' => 0];
        $newInviteCount = count($inviteSync['new_member_ids'] ?? []) + count($inviteSync['reactivated_member_ids'] ?? []) + count($inviteSync['new_group_ids'] ?? []) + count($inviteSync['reactivated_group_ids'] ?? []);
        if (!empty($input['notify_new_invitees']) && $newInviteCount > 0) {
            $notifyResult = (new NotificationService())->notifyWalkInvitations($walkId, (int)$admin['id']);
        }
        $generalNotifyResult = pyrk_send_walk_general_notification($pdo, $walkId, $input, (int)$admin['id']);
        // Editing a walk may also send an explicitly requested WhatsApp update.
        // The UI never auto-selects WhatsApp in edit mode, and the queue service keeps
        // all existing batch, daily-limit, duplicate, validation, and cron protections.
        $whatsAppNotifyResult = pyrk_queue_walk_whatsapp_notification($pdo, $walkId, $input, (int)$admin['id']);

        Response::ok([
            'walk_id' => $walkId,
            'invite_sync' => $inviteSync,
            'invite_notifications' => $notifyResult,
            'general_notification' => $generalNotifyResult,
            'whatsapp_notification' => $whatsAppNotifyResult,
        ], 'Walk updated.' . pyrk_invite_notification_message($notifyResult, $newInviteCount) . pyrk_walk_general_notification_message($generalNotifyResult) . pyrk_walk_whatsapp_notification_message($whatsAppNotifyResult));
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not update walk: ' . $e->getMessage());
    }
}

if ($method === 'POST' && $action === 'save_invitations') {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();
    $walkId = (int)($input['walk_id'] ?? 0);
    if (!$walkId) Response::error('Missing walk id.');
    if (!pyrk_walk_user_has_own_walk_full_access($pdo, $walkId, $admin)) {
        Response::error('You can update invitations only for walks you created or walks where you are the assigned manager.', 403);
    }
    $accessType = in_array(($input['access_type'] ?? 'open'), ['open','invitation_only'], true) ? $input['access_type'] : 'open';
    $memberIds = $input['invite_member_ids'] ?? [];
    $groupIds = $input['invite_group_ids'] ?? [];
    if (!is_array($memberIds)) $memberIds = array_filter(array_map('intval', explode(',', (string)$memberIds)));
    if (!is_array($groupIds)) $groupIds = array_filter(array_map('intval', explode(',', (string)$groupIds)));
    $pdo->beginTransaction();
    try {
        $pdo->prepare('UPDATE walks SET access_type = ?, invite_note = ?, allow_self_checkin = ?, updated_at = NOW() WHERE id = ?')->execute([$accessType, trim((string)($input['invite_note'] ?? '')), !empty($input['allow_self_checkin']) ? 1 : 0, $walkId]);
        $inviteSync = pyrk_sync_walk_invitations($pdo, $walkId, $memberIds, $groupIds, (int)$admin['id']);
        $pdo->commit();
        Security::audit((int)$admin['id'], 'walk_invitations_updated', 'walks', $walkId, ['access_type' => $accessType, 'allow_self_checkin' => !empty($input['allow_self_checkin']) ? 1 : 0, 'members' => count($memberIds), 'groups' => count($groupIds)]);
        $notifyResult = ['sent' => 0, 'logged' => 0, 'failed' => 0, 'skipped' => 0];
        $newInviteCount = count($inviteSync['new_member_ids'] ?? []) + count($inviteSync['reactivated_member_ids'] ?? []) + count($inviteSync['new_group_ids'] ?? []) + count($inviteSync['reactivated_group_ids'] ?? []);
        if (!empty($input['notify_new_invitees']) && $newInviteCount > 0) {
            $notifyResult = (new NotificationService())->notifyWalkInvitations($walkId, (int)$admin['id']);
        }
        Response::ok(['invite_sync' => $inviteSync, 'invite_notifications' => $notifyResult], 'Invitations updated.' . pyrk_invite_notification_message($notifyResult, $newInviteCount));
    } catch (Throwable $e) {
        $pdo->rollBack();
        Response::error('Could not update invitations: ' . $e->getMessage());
    }
}


if ($method === 'POST' && ($action === 'archive' || $action === 'archive_walk')) {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();

    $walkId = (int)($input['id'] ?? $input['walk_id'] ?? 0);
    if (!$walkId) Response::error('Missing walk id.');

    $stmt = $pdo->prepare('SELECT * FROM walks WHERE id = ? LIMIT 1');
    $stmt->execute([$walkId]);
    $before = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$before) Response::error('Walk not found.', 404);

    if (!pyrk_walk_user_can_archive($pdo, $walkId, $admin)) {
        Response::error('You can archive only walks you created, walks where you are the assigned manager, or all walks if you have global walk management permission.', 403);
    }

    if (strtolower((string)($before['status'] ?? '')) === 'archived' || (!empty($before['archived_at'] ?? null))) {
        Response::ok([], 'Walk is already archived.');
    }

    $cols = pyrk_walk_archive_columns($pdo);
    $reason = trim((string)($input['reason'] ?? $input['archive_reason'] ?? ''));
    if (function_exists('mb_strlen') && mb_strlen($reason) > 1000) { $reason = mb_substr($reason, 0, 1000); } elseif (!function_exists('mb_strlen') && strlen($reason) > 1000) { $reason = substr($reason, 0, 1000); }

    $sets = ['status = "archived"', 'updated_at = NOW()'];
    $params = [];
    if ($cols['archived_at']) $sets[] = 'archived_at = NOW()';
    if ($cols['archived_by']) { $sets[] = 'archived_by = ?'; $params[] = (int)$admin['id']; }
    if ($cols['archive_reason']) { $sets[] = 'archive_reason = ?'; $params[] = $reason; }
    if ($cols['archived_previous_status']) { $sets[] = 'archived_previous_status = ?'; $params[] = (string)($before['status'] ?? 'closed'); }
    $params[] = $walkId;

    try {
        $pdo->beginTransaction();
        $pdo->prepare('UPDATE walks SET ' . implode(', ', $sets) . ' WHERE id = ?')->execute($params);
        Security::audit((int)$admin['id'], 'walk_archived', 'walks', $walkId, ['title' => $before['title'] ?? null, 'reason' => $reason], $before);
        $pdo->commit();
        Response::ok([], 'Walk archived. It is now hidden from active client/admin walk lists.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not archive walk: ' . $e->getMessage());
    }
}

if ($method === 'POST' && ($action === 'restore' || $action === 'restore_walk')) {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();

    $walkId = (int)($input['id'] ?? $input['walk_id'] ?? 0);
    if (!$walkId) Response::error('Missing walk id.');

    $stmt = $pdo->prepare('SELECT * FROM walks WHERE id = ? LIMIT 1');
    $stmt->execute([$walkId]);
    $before = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$before) Response::error('Walk not found.', 404);

    if (!pyrk_walk_user_can_archive($pdo, $walkId, $admin)) {
        Response::error('You can restore only walks you created, walks where you are the assigned manager, or all walks if you have global walk management permission.', 403);
    }

    $cols = pyrk_walk_archive_columns($pdo);
    $previous = $cols['archived_previous_status'] ? strtolower(trim((string)($before['archived_previous_status'] ?? ''))) : '';
    if (!in_array($previous, ['draft','open','closed','completed','cancelled'], true)) {
        $previous = 'closed';
    }

    $sets = ['status = ?', 'updated_at = NOW()'];
    $params = [$previous];
    if ($cols['archived_at']) $sets[] = 'archived_at = NULL';
    if ($cols['archived_by']) $sets[] = 'archived_by = NULL';
    if ($cols['archive_reason']) $sets[] = 'archive_reason = NULL';
    if ($cols['archived_previous_status']) $sets[] = 'archived_previous_status = NULL';
    $params[] = $walkId;

    try {
        $pdo->beginTransaction();
        $pdo->prepare('UPDATE walks SET ' . implode(', ', $sets) . ' WHERE id = ?')->execute($params);
        Security::audit((int)$admin['id'], 'walk_restored', 'walks', $walkId, ['title' => $before['title'] ?? null, 'restored_status' => $previous], $before);
        $pdo->commit();
        Response::ok([], 'Walk restored.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not restore walk: ' . $e->getMessage());
    }
}

if ($method === 'POST' && $action === 'delete') {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();

    $walkId = (int)($input['id'] ?? $input['walk_id'] ?? 0);
    if (!$walkId) Response::error('Missing walk id.');
    if (!pyrk_walk_user_has_own_walk_full_access($pdo, $walkId, $admin) && !(Auth::hasPermission('walks.delete', $admin) && pyrk_walk_user_can_view_all_walks($admin))) {
        Response::error('You can delete only walks you created or walks where you are the assigned manager. To delete other walks, grant walks.delete with walks.view_all or walks.manage.', 403);
    }

    $stmt = $pdo->prepare('SELECT * FROM walks WHERE id = ? LIMIT 1');
    $stmt->execute([$walkId]);
    $before = $stmt->fetch();
    if (!$before) Response::error('Walk not found.', 404);

    try {
        $pdo->beginTransaction();
        // Related rows should cascade on modern installs; this extra cleanup protects upgraded databases.
        foreach (['walk_invitations','walk_photo_voting_groups','walk_photo_voting_settings'] as $table) {
            try { $pdo->prepare("DELETE FROM {$table} WHERE walk_id = ?")->execute([$walkId]); } catch (Throwable) {}
        }
        $pdo->prepare('DELETE FROM walks WHERE id = ?')->execute([$walkId]);
        Security::audit((int)$admin['id'], 'walk_deleted', 'walks', $walkId, ['title' => $before['title'] ?? null], $before);
        $pdo->commit();
        Response::ok([], 'Walk deleted.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not delete walk: ' . $e->getMessage());
    }
}


if ($method === 'POST' && ($action === 'unregister_member' || $action === 'unregister')) {
    $admin = Auth::requireUser();
    Csrf::validateFromRequest();

    $walkId = (int)($input['walk_id'] ?? 0);
    $userId = (int)($input['user_id'] ?? $input['member_id'] ?? 0);
    $registrationId = (int)($input['registration_id'] ?? 0);

    if (!$walkId) Response::error('Missing walk id.');
    if (!$userId && !$registrationId) Response::error('Missing member id.');

    $where = $registrationId > 0 ? 'wr.id = ?' : 'wr.walk_id = ? AND wr.user_id = ?';
    $params = $registrationId > 0 ? [$registrationId] : [$walkId, $userId];

    $stmt = $pdo->prepare("SELECT wr.*, u.full_name, u.email, w.title AS walk_title FROM walk_registrations wr JOIN users u ON u.id = wr.user_id JOIN walks w ON w.id = wr.walk_id WHERE {$where} LIMIT 1");
    $stmt->execute($params);
    $registration = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$registration) Response::error('Registration not found.', 404);
    $walkId = (int)$registration['walk_id'];
    $userId = (int)$registration['user_id'];

    if (!pyrk_walk_user_has_own_walk_full_access($pdo, $walkId, $admin) && !pyrk_walk_user_can_view_all_walks($admin)) {
        Response::error('You can unregister members only from walks you created, manage, or are globally allowed to manage.', 403);
    }

    if (($registration['registration_status'] ?? '') === 'cancelled') {
        Response::ok([], 'Member is already unregistered from this walk.');
    }

    try {
        $pdo->beginTransaction();

        $pdo->prepare('UPDATE walk_registrations SET registration_status = "cancelled", cancelled_at = NOW(), updated_at = NOW(), notes = CONCAT(COALESCE(notes, ""), CASE WHEN COALESCE(notes, "") = "" THEN "" ELSE "\n" END, "Unregistered by admin") WHERE id = ?')
            ->execute([(int)$registration['id']]);

        // If attendance was accidentally pre-created but not checked in, remove it.
        try {
            $pdo->prepare('DELETE FROM attendance WHERE walk_id = ? AND user_id = ? AND check_in_time IS NULL AND finish_time IS NULL')
                ->execute([$walkId, $userId]);
        } catch (Throwable) {}

        Security::audit((int)$admin['id'], 'walk_member_unregistered', 'walk_registrations', (int)$registration['id'], [
            'walk_id' => $walkId,
            'user_id' => $userId,
            'member' => $registration['full_name'] ?? null,
            'walk' => $registration['walk_title'] ?? null,
        ], $registration);

        $pdo->commit();
        Response::ok([], 'Member unregistered from this walk.');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error('Could not unregister member: ' . $e->getMessage());
    }
}


function pyrk_walk_ensure_unpaid_payment(PDO $pdo, int $walkId, int $userId): void
{
    if ($walkId <= 0 || $userId <= 0) return;

    try {
        $table = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "walk_payments"');
        $table->execute();
        if ((int)$table->fetchColumn() <= 0) return;
    } catch (Throwable) {
        return;
    }

    try {
        $walkStmt = $pdo->prepare('SELECT id, payment_model, payment_amount, payment_currency FROM walks WHERE id = ? LIMIT 1');
        $walkStmt->execute([$walkId]);
        $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
        if (!$walk) return;

        if (strtolower((string)($walk['payment_model'] ?? 'free')) !== 'paid' || (float)($walk['payment_amount'] ?? 0) <= 0) {
            return;
        }

        $regStmt = $pdo->prepare('SELECT id FROM walk_registrations WHERE walk_id = ? AND user_id = ? AND registration_status IN ("registered","approved","waitlisted") LIMIT 1');
        $regStmt->execute([$walkId, $userId]);
        $registrationId = (int)($regStmt->fetchColumn() ?: 0);
        if ($registrationId <= 0) return;

        $amountDue = round(max(0, (float)($walk['payment_amount'] ?? 0)), 2);
        $currency = strtoupper(trim((string)($walk['payment_currency'] ?? 'LBP'))) ?: 'LBP';

        $stmt = $pdo->prepare('INSERT INTO walk_payments
            (walk_id, registration_id, user_id, amount_due, amount_paid, currency, payment_type, payment_status, reference_no, notes, paid_at, recorded_by, created_at, updated_at)
            VALUES (?, ?, ?, ?, 0.00, ?, "cash", "unpaid", NULL, NULL, NULL, NULL, NOW(), NOW())
            ON DUPLICATE KEY UPDATE
                registration_id = VALUES(registration_id),
                amount_due = CASE WHEN payment_status = "unpaid" AND amount_paid = 0 THEN VALUES(amount_due) ELSE amount_due END,
                currency = CASE WHEN payment_status = "unpaid" AND amount_paid = 0 THEN VALUES(currency) ELSE currency END,
                updated_at = NOW()');
        $stmt->execute([$walkId, $registrationId, $userId, $amountDue, $currency]);
    } catch (Throwable) {
        // Registration should never fail just because the payment ledger could not be prepared.
    }
}


if ($method === 'POST' && $action === 'sign_and_register') {
    $user = Auth::requireUser();
    Csrf::validateFromRequest();

    $walkId = (int)($input['walk_id'] ?? 0);
    $signedName = trim((string)($input['signed_name'] ?? ''));
    $signatureData = trim((string)($input['signature_data'] ?? ''));
    if ($walkId <= 0) Response::error('Missing walk id.');

    $walkStmt = $pdo->prepare('SELECT id, waiver_required FROM walks WHERE id = ? LIMIT 1');
    $walkStmt->execute([$walkId]);
    $walk = $walkStmt->fetch(PDO::FETCH_ASSOC);
    if (!$walk) Response::error('Walk not found.', 404);

    $existingRegistration = pyrk_user_registration($pdo, $walkId, $user);
    $waiverAlreadySigned = pyrk_user_waiver_signed($pdo, $walkId, $user);

    if (!$waiverAlreadySigned && (int)($walk['waiver_required'] ?? 0) === 1) {
        if ($signedName === '') Response::error('Please type your full name.');
        if ($signatureData === '') Response::error('Please draw your digital signature before joining the walk.');
        if (!preg_match('/^data:image\/(png|jpeg);base64,/', $signatureData)) Response::error('Invalid digital signature format.');
        if (strlen($signatureData) > 1000000) Response::error('Digital signature image is too large.');
    }

    if ($existingRegistration && $waiverAlreadySigned) {
        Response::ok([
            'already_registered' => true,
            'registered' => true,
            'waiver_signed' => true,
            'registration' => $existingRegistration,
        ], 'Waiver and registration are already complete.');
    }

    $pdo->beginTransaction();
    try {
        $attendance = new AttendanceService();
        if (!$waiverAlreadySigned && (int)($walk['waiver_required'] ?? 0) === 1) {
            $attendance->signWaiver($walkId, (int)$user['id'], $signedName, $signatureData);
        }

        if (!$existingRegistration) {
            $attendance->registerForWalk($walkId, (int)$user['id']);
        }

        pyrk_walk_ensure_unpaid_payment($pdo, $walkId, (int)$user['id']);
        $registration = pyrk_user_registration($pdo, $walkId, $user);
        $pdo->commit();

        try {
            (new NotificationService())->notifyWalkRegistered($walkId, (int)$user['id']);
        } catch (Throwable) {}

        Response::ok([
            'registered' => true,
            'waiver_signed' => (int)($walk['waiver_required'] ?? 0) === 1,
            'registration' => $registration,
        ], (int)($walk['waiver_required'] ?? 0) === 1
            ? 'Waiver signed and registration completed successfully.'
            : 'Registration completed successfully.');
    } catch (RuntimeException $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        Response::error($e->getMessage(), 422);
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        error_log('Pyrk sign_and_register failed for walk ' . $walkId . ', user ' . (int)$user['id'] . ': ' . $e->getMessage());
        Response::error('Could not complete waiver and registration. Please try again.', 500);
    }
}


if ($method === 'POST' && $action === 'register') {
    $user = Auth::requireUser();
    Csrf::validateFromRequest();
    $walkId = (int)($input['walk_id'] ?? 0);
    if (!$walkId) Response::error('Missing walk id.');

    $existing = pyrk_user_registration($pdo, $walkId, $user);
    if ($existing) {
        Response::ok(['already_registered' => true, 'registration' => $existing], 'You are already registered for this walk.');
    }

    $walkForWaiver = $pdo->prepare('SELECT id, waiver_required FROM walks WHERE id = ? LIMIT 1');
    $walkForWaiver->execute([$walkId]);
    $waiverWalk = $walkForWaiver->fetch(PDO::FETCH_ASSOC);
    if (!$waiverWalk) Response::error('Walk not found.', 404);
    if ((int)($waiverWalk['waiver_required'] ?? 0) === 1 && !pyrk_user_waiver_signed($pdo, $walkId, $user)) {
        Response::error('Waiver must be signed before registration for this walk. Please sign the waiver, then register.', 422);
    }

    $couponCode = trim((string)($input['coupon_code'] ?? ''));
    $couponPreview = null;
    if ($couponCode !== '') {
        try {
            $couponPreview = (new CouponService())->validateForWalk((int)$user['id'], $walkId, $couponCode);
        } catch (Throwable $e) {
            Response::error($e->getMessage());
        }
    }

    try {
        (new AttendanceService())->registerForWalk($walkId, (int)$user['id']);
        pyrk_walk_ensure_unpaid_payment($pdo, $walkId, (int)$user['id']);
    } catch (RuntimeException $e) {
        Response::error($e->getMessage(), 422);
    } catch (Throwable $e) {
        Response::error('Could not register for walk: ' . $e->getMessage(), 500);
    }

    $registration = pyrk_user_registration($pdo, $walkId, $user);

    $couponRedemption = null;
    if ($couponCode !== '' && $registration && !empty($registration['id'])) {
        try {
            $couponRedemption = (new CouponService())->redeem((int)$user['id'], $walkId, (int)$registration['id'], $couponCode);
        } catch (Throwable $e) {
            Response::error('Registered, but coupon could not be applied: ' . $e->getMessage());
        }
    }

    try {
        (new NotificationService())->notifyWalkRegistered($walkId, (int)$user['id']);
    } catch (Throwable) {}

    Response::ok(['registered' => true, 'registration' => $registration, 'coupon' => $couponRedemption ?: $couponPreview], $couponRedemption ? 'Registered for walk. Coupon applied.' : 'Registered for walk.');
}

Response::error('Unsupported walks action.', 404);
