<?php
// HomeSyncHub CommandCenter PWA - clean PHP + JSON family hub.
// PHP 8.0+ recommended. No database required.

declare(strict_types=1);

const APP_NAME = 'HomeSyncHub';
const APP_VERSION = 'CommandCenter PWA 9.4 Account Type Prompt and Eye Visibility';
const APP_TZ = 'Asia/Singapore';
const STORE_FILE = __DIR__ . '/../data/store.json';

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

function app_now(): DateTimeImmutable { return new DateTimeImmutable('now', new DateTimeZone(APP_TZ)); }
function e($value): string { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); }
function uid(string $prefix = ''): string { return $prefix . bin2hex(random_bytes(8)); }

function default_store(): array
{
    return [
        'schema_version' => 8,
        'users' => [],
        'workspaces' => [],
        'workspace_members' => [],
        'items' => [],
        'list_items' => [],
        'settings' => [],
        'activity' => [],
        'connections' => [],
        'share_invites' => [],
        'photos' => [],
        'workspace_history' => [],
        'password_reset_tokens' => [],
        'mail_outbox' => [],
        'proof_uploads' => [],
        'chat_uploads' => [],
        'device_unlocks' => [],
        'remember_login_tokens' => [],
    ];
}


function default_profiles(): array
{
    return [
        ['name' => 'Family', 'color' => '#49d9ff', 'avatar' => '🏠'],
        ['name' => 'Parent', 'color' => '#a78bfa', 'avatar' => '👤'],
        ['name' => 'Kid', 'color' => '#fbbf24', 'avatar' => '⭐'],
    ];
}

function default_settings(): array
{
    return [
        'theme' => 'light',
        'view_mode' => 'auto',
        'accent' => 'ocean',
        'profiles' => default_profiles(),
        'daily_goal' => 40,
        'reminder_minutes' => 30,
        'home_message' => 'The family command center is ready.',
        'week_starts' => 'monday',
        'weather_location' => 'Singapore',
        'weather_summary' => 'Partly cloudy • 31°C • light breeze',
        'sleep_start' => '22:30',
        'sleep_end' => '06:30',
        'sleep_enabled' => true,
        'parental_lock' => false,
        'device_name' => 'Kitchen Display',
        'screensaver_enabled' => true,
        'menu_library' => default_menu_library(),
        'meal_plan' => default_weekly_meal_plan(),
        'device_unlock_enabled' => true,
        'device_unlock_minutes' => 120,
        'device_unlock_webhook_url' => '',
    ];
}

function ensure_store_file(): void
{
    $dir = dirname(STORE_FILE);
    if (!is_dir($dir)) mkdir($dir, 0775, true);
    if (!file_exists(STORE_FILE)) file_put_contents(STORE_FILE, json_encode(default_store(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}

function normalize_profiles($profiles): array
{
    if (!is_array($profiles)) return default_profiles();
    $clean = [];
    foreach ($profiles as $profile) {
        if (is_string($profile)) {
            $name = trim($profile);
            if ($name !== '') $clean[] = ['name' => $name, 'color' => profile_color($name), 'avatar' => '👤'];
            continue;
        }
        if (!is_array($profile)) continue;
        $name = trim((string)($profile['name'] ?? ''));
        if ($name === '') continue;
        $color = trim((string)($profile['color'] ?? profile_color($name)));
        if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) $color = profile_color($name);
        $avatar = trim((string)($profile['avatar'] ?? '👤')) ?: '👤';
        $clean[] = ['name' => $name, 'color' => $color, 'avatar' => $avatar];
    }
    return $clean ?: default_profiles();
}

function normalize_store(array $store): array
{
    foreach (default_store() as $key => $value) {
        if (!array_key_exists($key, $store)) $store[$key] = $value;
        if (is_array($value) && !is_array($store[$key])) $store[$key] = $value;
    }

    $workspaceIds = [];
    foreach (($store['workspaces'] ?? []) as $idx => $workspace) {
        if (!is_array($workspace)) { unset($store['workspaces'][$idx]); continue; }
        $id = (string)($workspace['id'] ?? '');
        if ($id === '') { $id = uid('ws_'); $store['workspaces'][$idx]['id'] = $id; }
        $store['workspaces'][$idx]['name'] = trim((string)($workspace['name'] ?? 'Family Workspace')) ?: 'Family Workspace';
        $store['workspaces'][$idx]['join_code'] = normalize_join_code((string)($workspace['join_code'] ?? '')) ?: generate_join_code($store);
        $store['workspaces'][$idx]['status'] = in_array(($workspace['status'] ?? 'active'), ['active','archived'], true) ? $workspace['status'] : 'active';
        $store['workspaces'][$idx]['created_at'] = (string)($workspace['created_at'] ?? app_now()->format(DateTimeInterface::ATOM));
        $workspaceIds[$id] = true;
    }

    foreach (($store['users'] ?? []) as $idx => $existingUser) {
        if (!is_array($existingUser)) { unset($store['users'][$idx]); continue; }
        if (empty($existingUser['id'])) $store['users'][$idx]['id'] = uid('user_');
        if (!isset($existingUser['status'])) $store['users'][$idx]['status'] = 'active';
        if (!isset($store['users'][$idx]['profile_photo'])) $store['users'][$idx]['profile_photo'] = '';
        if (!isset($store['users'][$idx]['profile_background'])) $store['users'][$idx]['profile_background'] = '';
        if (!isset($store['users'][$idx]['two_factor_enabled'])) $store['users'][$idx]['two_factor_enabled'] = false;
        if (!isset($store['users'][$idx]['two_factor_secret'])) $store['users'][$idx]['two_factor_secret'] = '';
        if (!isset($store['users'][$idx]['two_factor_pending_secret'])) $store['users'][$idx]['two_factor_pending_secret'] = '';
        if (!isset($store['users'][$idx]['terms_accepted_at'])) $store['users'][$idx]['terms_accepted_at'] = '';
        if (!isset($store['users'][$idx]['account_type'])) $store['users'][$idx]['account_type'] = is_super_admin_role((string)($existingUser['role'] ?? '')) ? 'parent' : 'member';
        $oldRole = (string)($existingUser['role'] ?? 'Member');
        $store['users'][$idx]['role'] = normalize_system_role($oldRole);
        if (!isset($store['users'][$idx]['workspace_ids']) || !is_array($store['users'][$idx]['workspace_ids'])) $store['users'][$idx]['workspace_ids'] = [];
    }



    $cleanRememberTokens = [];
    $nowForRememberTokens = app_now();
    foreach (($store['remember_login_tokens'] ?? []) as $tokenRow) {
        if (!is_array($tokenRow)) continue;
        $expires = parse_dt((string)($tokenRow['expires_at'] ?? ''));
        if (!$expires || $expires < $nowForRememberTokens) continue;
        $selector = (string)($tokenRow['selector'] ?? '');
        $verifierHash = (string)($tokenRow['verifier_hash'] ?? '');
        $userIdForToken = (string)($tokenRow['user_id'] ?? '');
        if ($selector === '' || $verifierHash === '' || $userIdForToken === '') continue;
        $cleanRememberTokens[] = array_merge([
            'id' => uid('remember_'),
            'created_at' => app_now()->format(DateTimeInterface::ATOM),
            'last_used_at' => '',
        ], $tokenRow);
    }
    $store['remember_login_tokens'] = array_values($cleanRememberTokens);

    foreach (($store['items'] ?? []) as $idx => $item) {
        if (!is_array($item)) { unset($store['items'][$idx]); continue; }
        $type = (string)($item['type'] ?? 'task');
        $title = (string)($item['title'] ?? '');
        $category = (string)($item['category'] ?? '');
        $notes = (string)($item['notes'] ?? '');
        if (!isset($store['items'][$idx]['proof_required'])) $store['items'][$idx]['proof_required'] = inferred_proof_required($type, $title, $category, $notes);
        if (!isset($store['items'][$idx]['proof_status'])) $store['items'][$idx]['proof_status'] = 'not_required';
        if (!isset($store['items'][$idx]['proof_upload_id'])) $store['items'][$idx]['proof_upload_id'] = '';
        if (!isset($store['items'][$idx]['verification_message'])) $store['items'][$idx]['verification_message'] = '';
        if (!isset($store['items'][$idx]['device_unlock_id'])) $store['items'][$idx]['device_unlock_id'] = '';
        if ($store['items'][$idx]['proof_required']) {
            $status = (string)($store['items'][$idx]['proof_status'] ?? 'required');
            $hasProof = trim((string)($store['items'][$idx]['proof_upload_id'] ?? '')) !== '';
            $isDone = (($store['items'][$idx]['status'] ?? 'pending') === 'done');
            if ($isDone && in_array($status, ['verified_correct','uploaded','needs_review','incorrect','pending_parent_review'], true)) {
                $store['items'][$idx]['proof_status'] = 'parent_approved';
            } elseif ($hasProof && in_array($status, ['uploaded','needs_review','incorrect','verified_correct'], true)) {
                $store['items'][$idx]['proof_status'] = 'pending_parent_review';
            } elseif (!$hasProof && !in_array($status, ['required','not_required'], true)) {
                $store['items'][$idx]['proof_status'] = 'required';
            }
            $msg = (string)($store['items'][$idx]['verification_message'] ?? '');
            if (preg_match('/HTTP 429|could not be completed|not configured|could not produce/i', $msg)) {
                $store['items'][$idx]['verification_message'] = $hasProof ? 'Homework uploaded. Waiting for parent review.' : '';
            }
        }
    }

    $hasSuperadmin = false;
    foreach (($store['users'] ?? []) as $existingUser) {
        if (is_array($existingUser) && is_super_admin_role((string)($existingUser['role'] ?? ''))) { $hasSuperadmin = true; break; }
    }
    if (!$hasSuperadmin) {
        $superId = 'user_superadmin';
        $systemWsId = 'workspace_system';
        $store['users'][] = [
            'id' => $superId,
            'name' => 'HomeSyncHub Super Admin',
            'email' => 'superadmin@homesynchub.local',
            'role' => 'Super Admin',
            'status' => 'active',
            'workspace_ids' => [$systemWsId],
            'active_workspace_id' => $systemWsId,
            'password_hash' => password_hash('ChangeMe123!', PASSWORD_DEFAULT),
            'points' => 0,
            'stars' => 0,
            'streak' => 0,
            'last_completed_date' => '',
            'created_at' => app_now()->format(DateTimeInterface::ATOM),
            'must_change_password' => true,
            'profile_photo' => '',
            'profile_background' => '',
            'two_factor_enabled' => false,
            'two_factor_secret' => '',
            'two_factor_pending_secret' => '',
            'terms_accepted_at' => app_now()->format(DateTimeInterface::ATOM),
        ];
        if (empty($store['workspaces'][$systemWsId])) {
            $store['workspaces'][] = [
                'id' => $systemWsId,
                'name' => 'Super Admin Workspace',
                'join_code' => generate_join_code($store),
                'owner_user_id' => $superId,
                'created_by' => $superId,
                'status' => 'active',
                'created_at' => app_now()->format(DateTimeInterface::ATOM),
            ];
        }
        $store['workspace_members'][] = [
            'id' => 'member_superadmin',
            'workspace_id' => $systemWsId,
            'user_id' => $superId,
            'role' => 'Admin',
            'status' => 'active',
            'created_at' => app_now()->format(DateTimeInterface::ATOM),
        ];
        if (empty($store['settings'][$systemWsId])) $store['settings'][$systemWsId] = default_settings();
    }

    // Migration: older packages stored each hub as user.family_id and used the user id as the data scope.
    foreach (($store['users'] ?? []) as $idx => $user) {
        if (!is_array($user)) continue;
        $userId = (string)($user['id'] ?? '');
        if ($userId === '') continue;
        $existingUserMemberships = array_values(array_filter($store['workspace_members'] ?? [], fn($m) => is_array($m) && ($m['user_id'] ?? '') === $userId));
        if ($existingUserMemberships) {
            $firstActiveWorkspace = '';
            foreach ($existingUserMemberships as $m) {
                $wid = (string)($m['workspace_id'] ?? '');
                if ($wid === '') continue;
                if (!in_array($wid, $store['users'][$idx]['workspace_ids'] ?? [], true)) $store['users'][$idx]['workspace_ids'][] = $wid;
                if ($firstActiveWorkspace === '' && ($m['status'] ?? 'active') === 'active') $firstActiveWorkspace = $wid;
            }
            if (empty($store['users'][$idx]['active_workspace_id']) && $firstActiveWorkspace !== '') $store['users'][$idx]['active_workspace_id'] = $firstActiveWorkspace;
            continue;
        }
        $legacyHub = (string)($user['family_id'] ?? $userId);
        $workspaceId = $legacyHub ?: $userId;
        if (!isset($workspaceIds[$workspaceId])) {
            $store['workspaces'][] = [
                'id' => $workspaceId,
                'name' => trim((string)($user['workspace_name'] ?? (($user['name'] ?? 'Family') . "'s Workspace"))) ?: 'Family Workspace',
                'join_code' => generate_join_code($store),
                'owner_user_id' => $userId,
                'created_by' => $userId,
                'status' => 'active',
                'created_at' => (string)($user['created_at'] ?? app_now()->format(DateTimeInterface::ATOM)),
            ];
            $workspaceIds[$workspaceId] = true;
        }
        $memberRole = legacy_workspace_role((string)($user['role'] ?? ''), (string)($user['role'] ?? ''));
        if (!membership_exists($store, $workspaceId, $userId)) {
            $store['workspace_members'][] = [
                'id' => uid('member_'),
                'workspace_id' => $workspaceId,
                'user_id' => $userId,
                'role' => $memberRole,
                'status' => ($user['status'] ?? 'active') === 'archived' ? 'archived' : 'active',
                'created_at' => (string)($user['created_at'] ?? app_now()->format(DateTimeInterface::ATOM)),
            ];
        }
        if (empty($store['users'][$idx]['active_workspace_id'])) $store['users'][$idx]['active_workspace_id'] = $workspaceId;
        if (!in_array($workspaceId, $store['users'][$idx]['workspace_ids'] ?? [], true)) $store['users'][$idx]['workspace_ids'][] = $workspaceId;
        if (empty($store['settings'][$workspaceId])) $store['settings'][$workspaceId] = $store['settings'][$userId] ?? default_settings();
    }

    // Normalize member records and maintain user workspace_ids.
    foreach (($store['workspace_members'] ?? []) as $idx => $m) {
        if (!is_array($m)) { unset($store['workspace_members'][$idx]); continue; }
        if (empty($m['id'])) $store['workspace_members'][$idx]['id'] = uid('member_');
        $store['workspace_members'][$idx]['role'] = normalize_workspace_role((string)($m['role'] ?? 'Member'));
        $store['workspace_members'][$idx]['status'] = in_array(($m['status'] ?? 'active'), ['active','archived','left'], true) ? $m['status'] : 'active';
        $store['workspace_members'][$idx]['created_at'] = (string)($m['created_at'] ?? app_now()->format(DateTimeInterface::ATOM));
        $uid = (string)($m['user_id'] ?? ''); $wid = (string)($m['workspace_id'] ?? '');
        if ($uid !== '' && $wid !== '') {
            foreach ($store['users'] as $uIdx => $u) {
                if (($u['id'] ?? '') === $uid && !in_array($wid, $store['users'][$uIdx]['workspace_ids'] ?? [], true)) $store['users'][$uIdx]['workspace_ids'][] = $wid;
            }
        }
    }

    foreach (($store['settings'] ?? []) as $uid => $settings) {
        if (!is_array($settings)) $settings = [];
        if (isset($settings['members']) && !isset($settings['profiles'])) $settings['profiles'] = $settings['members'];
        $settings = array_merge(default_settings(), $settings);
        $settings['profiles'] = normalize_profiles($settings['profiles']);
        $settings['menu_library'] = normalize_menu_library($settings['menu_library'] ?? []);
        $settings['meal_plan'] = normalize_weekly_meal_plan($settings['meal_plan'] ?? [], $settings['menu_library']);
        $store['settings'][$uid] = $settings;
    }
    $store['workspaces'] = array_values($store['workspaces']);
    $store['workspace_members'] = array_values($store['workspace_members']);
    $store['schema_version'] = 8;
    return $store;
}

function persist_store_raw(array $store): void
{
    ensure_store_file();
    $fp = fopen(STORE_FILE, 'c+');
    if (!$fp) throw new RuntimeException('Unable to open storage file.');
    flock($fp, LOCK_EX);
    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, json_encode($store, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    fflush($fp);
    flock($fp, LOCK_UN);
    fclose($fp);
}

function read_store(): array
{
    ensure_store_file();
    $raw = file_get_contents(STORE_FILE);
    $store = json_decode($raw ?: '', true);
    if (!is_array($store)) $store = default_store();
    $normalized = normalize_store($store);
    if (json_encode($normalized, JSON_UNESCAPED_SLASHES) !== json_encode($store, JSON_UNESCAPED_SLASHES)) {
        persist_store_raw($normalized);
    }
    return $normalized;
}

function write_store(array $store): void
{
    persist_store_raw(normalize_store($store));
}

function flash(string $type, string $message): void { $_SESSION['flash'][] = ['type' => $type, 'message' => $message]; }
function flashes(): array { $m = $_SESSION['flash'] ?? []; unset($_SESSION['flash']); return $m; }
function redirect_to(string $page = 'dashboard', array $params = []): never { header('Location: index.php?' . http_build_query(array_merge(['page' => $page], $params))); exit; }

function user_by_email(array $store, string $email): ?array
{
    $needle = strtolower(trim($email));
    foreach ($store['users'] as $user) if (strtolower($user['email'] ?? '') === $needle) return $user;
    return null;
}

function user_by_id(array $store, string $id): ?array
{
    foreach (($store['users'] ?? []) as $user) if (($user['id'] ?? '') === $id) return $user;
    return null;
}


function remember_cookie_name(): string { return 'homesynchub_remember_login'; }

function cookie_path_for_app(): string
{
    $path = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? '/'), '/\\');
    return $path === '' ? '/' : $path;
}

function set_remember_cookie(string $value, int $expires): void
{
    setcookie(remember_cookie_name(), $value, [
        'expires' => $expires,
        'path' => cookie_path_for_app(),
        'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
}

function clear_remember_cookie(): void { set_remember_cookie('', time() - 3600); }

function issue_remember_login_token(array &$store, string $userId): void
{
    $selector = bin2hex(random_bytes(9));
    $verifier = bin2hex(random_bytes(32));
    $expires = app_now()->modify('+30 days');
    $store['remember_login_tokens'] = array_values(array_filter($store['remember_login_tokens'] ?? [], function ($row) use ($userId) {
        return is_array($row) && (($row['user_id'] ?? '') !== $userId);
    }));
    $store['remember_login_tokens'][] = [
        'id' => uid('remember_'),
        'user_id' => $userId,
        'selector' => $selector,
        'verifier_hash' => hash('sha256', $verifier),
        'expires_at' => $expires->format(DateTimeInterface::ATOM),
        'created_at' => app_now()->format(DateTimeInterface::ATOM),
        'last_used_at' => '',
    ];
    set_remember_cookie($selector . ':' . $verifier, $expires->getTimestamp());
}

function clear_current_remember_login(array &$store): void
{
    $cookie = (string)($_COOKIE[remember_cookie_name()] ?? '');
    if (str_contains($cookie, ':')) {
        [$selector] = explode(':', $cookie, 2);
        foreach (($store['remember_login_tokens'] ?? []) as $idx => $row) {
            if (($row['selector'] ?? '') === $selector) unset($store['remember_login_tokens'][$idx]);
        }
        $store['remember_login_tokens'] = array_values($store['remember_login_tokens'] ?? []);
    }
    clear_remember_cookie();
}

function auto_login_from_remember_cookie(): ?array
{
    $cookie = (string)($_COOKIE[remember_cookie_name()] ?? '');
    if ($cookie === '' || !str_contains($cookie, ':')) return null;
    [$selector, $verifier] = explode(':', $cookie, 2);
    if ($selector === '' || $verifier === '') { clear_remember_cookie(); return null; }
    $store = read_store();
    $now = app_now();
    foreach (($store['remember_login_tokens'] ?? []) as $idx => $row) {
        if (($row['selector'] ?? '') !== $selector) continue;
        $expires = parse_dt((string)($row['expires_at'] ?? ''));
        if (!$expires || $expires < $now || !hash_equals((string)($row['verifier_hash'] ?? ''), hash('sha256', $verifier))) {
            unset($store['remember_login_tokens'][$idx]);
            write_store($store);
            clear_remember_cookie();
            return null;
        }
        $user = user_by_id($store, (string)($row['user_id'] ?? ''));
        if (!$user || ($user['status'] ?? 'active') !== 'active') {
            unset($store['remember_login_tokens'][$idx]);
            write_store($store);
            clear_remember_cookie();
            return null;
        }
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['active_workspace_id'] = current_workspace_id_for_user($user);
        $store['remember_login_tokens'][$idx]['last_used_at'] = app_now()->format(DateTimeInterface::ATOM);
        write_store($store);
        return $user;
    }
    clear_remember_cookie();
    return null;
}

function mask_email_address(string $email): string
{
    $email = trim($email);
    if (!str_contains($email, '@')) return $email;
    [$local, $domain] = explode('@', $email, 2);
    $first = substr($local, 0, 1) ?: '*';
    $last = strlen($local) > 2 ? substr($local, -1) : '';
    return $first . str_repeat('•', max(2, strlen($local) - 2)) . $last . '@' . $domain;
}

function current_user(): ?array
{
    $id = $_SESSION['user_id'] ?? null;
    if (!$id) return auto_login_from_remember_cookie();
    foreach (read_store()['users'] as $user) if (($user['id'] ?? '') === $id) return $user;
    return auto_login_from_remember_cookie();
}

function require_user(): array { $u = current_user(); if (!$u) redirect_to('login'); return $u; }

function settings_for(string $userId): array
{
    $store = read_store();
    $settings = array_merge(default_settings(), is_array($store['settings'][$userId] ?? null) ? $store['settings'][$userId] : []);
    $settings['profiles'] = normalize_profiles($settings['profiles']);
    $settings['menu_library'] = normalize_menu_library($settings['menu_library'] ?? []);
    $settings['meal_plan'] = normalize_weekly_meal_plan($settings['meal_plan'] ?? [], $settings['menu_library']);
    return $settings;
}
function save_settings(string $userId, array $settings): void { $s = read_store(); $s['settings'][$userId] = array_merge(default_settings(), $settings); write_store($s); }
function profiles_for(array $settings): array { return normalize_profiles($settings['profiles'] ?? []); }
function member_names(array $settings): array { return array_map(fn($p) => $p['name'], profiles_for($settings)); }

function profile_for_member(array $settings, string $member): array
{
    foreach (profiles_for($settings) as $profile) if (($profile['name'] ?? '') === $member) return $profile;
    return ['name' => $member ?: 'Family', 'color' => profile_color($member ?: 'Family'), 'avatar' => '👤'];
}
function profile_color(string $member): string
{
    $palette = ['#49d9ff', '#a78bfa', '#fbbf24', '#44d7a8', '#fb7185', '#38bdf8', '#f0abfc', '#fb923c'];
    return $palette[abs(crc32($member)) % count($palette)];
}


function meal_days(): array { return ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']; }
function meal_slots(): array { return ['Breakfast','Lunch','Snacks','Dinner']; }
function meal_day_color(string $day): string
{
    return match($day) {
        'Monday' => '#ff4fa3',
        'Tuesday' => '#fb923c',
        'Wednesday' => '#22c55e',
        'Thursday' => '#38bdf8',
        'Friday' => '#a855f7',
        'Saturday' => '#fb7185',
        'Sunday' => '#fbbf24',
        default => '#49d9ff',
    };
}
function normalize_meal_slot(string $slot): string
{
    $slot = trim($slot);
    if (strcasecmp($slot, 'Snack') === 0) return 'Snacks';
    foreach (meal_slots() as $valid) if (strcasecmp($slot, $valid) === 0) return $valid;
    return 'Breakfast';
}
function meal_image_path(string $id): string { return 'assets/meals/' . preg_replace('/[^a-z0-9_\-]/i', '', $id) . '.png'; }
function save_uploaded_menu_image(string $menuId): string
{
    if (empty($_FILES['menu_image']) || !is_array($_FILES['menu_image']) || (int)($_FILES['menu_image']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
        return '';
    }
    $file = $_FILES['menu_image'];
    if ((int)($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
        flash('error', 'Menu image upload failed. Please try a PNG, JPG, JPEG, or WEBP file.');
        return '';
    }
    if ((int)($file['size'] ?? 0) > 5 * 1024 * 1024) {
        flash('error', 'Menu image is too large. Maximum size is 5 MB.');
        return '';
    }
    $tmp = (string)($file['tmp_name'] ?? '');
    $info = @getimagesize($tmp);
    if (!$info || empty($info['mime'])) {
        flash('error', 'Menu image must be a valid image file.');
        return '';
    }
    $ext = match($info['mime']) {
        'image/png' => 'png',
        'image/jpeg' => 'jpg',
        'image/webp' => 'webp',
        default => '',
    };
    if ($ext === '') {
        flash('error', 'Supported menu image formats: PNG, JPG, JPEG, WEBP.');
        return '';
    }
    $dir = dirname(__DIR__) . '/assets/uploads/meals';
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    $safeId = preg_replace('/[^a-z0-9_\-]/i', '', $menuId);
    $dest = $dir . '/' . $safeId . '.' . $ext;
    if (!@move_uploaded_file($tmp, $dest)) {
        flash('error', 'Could not save the uploaded menu image.');
        return '';
    }
    return 'assets/uploads/meals/' . $safeId . '.' . $ext;
}
function default_menu_library(): array
{
    $rows = [
        ['id'=>'tapsilog', 'title'=>'Tapsilog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','beef tapa','egg','tomato'], 'favorite'=>true],
        ['id'=>'longsilog', 'title'=>'Longsilog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','longganisa','egg','tomato'], 'favorite'=>true],
        ['id'=>'tosilog', 'title'=>'Tosilog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','tocino','egg','tomato'], 'favorite'=>true],
        ['id'=>'bangsilog', 'title'=>'Bangsilog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','bangus','egg','tomato'], 'favorite'=>false],
        ['id'=>'hotsilog', 'title'=>'Hotsilog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','hotdog','egg','banana ketchup'], 'favorite'=>false],
        ['id'=>'cornsilog', 'title'=>'Cornsilog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','corned beef','egg','onion'], 'favorite'=>false],
        ['id'=>'champorado_tuyo', 'title'=>'Champorado with Tuyo', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['glutinous rice','cocoa','milk','sugar','tuyo'], 'favorite'=>false],
        ['id'=>'ube_champorado', 'title'=>'Ube Champorado', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['glutinous rice','ube','coconut milk','sugar'], 'favorite'=>false],
        ['id'=>'arroz_caldo', 'title'=>'Arroz Caldo', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['rice','chicken','ginger','garlic','egg'], 'favorite'=>true],
        ['id'=>'lugaw_tokwa', 'title'=>'Lugaw with Tokwa', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['rice porridge','tofu','garlic','spring onion'], 'favorite'=>false],
        ['id'=>'pandesal_corned_beef', 'title'=>'Pandesal with Corned Beef', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['pandesal','corned beef','onion','egg'], 'favorite'=>false],
        ['id'=>'pandesal_cheese', 'title'=>'Pandesal with Cheese', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['pandesal','cheese','butter'], 'favorite'=>false],
        ['id'=>'tortang_talong', 'title'=>'Tortang Talong', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['eggplant','egg','onion','tomato'], 'favorite'=>false],
        ['id'=>'daing_na_bangus', 'title'=>'Daing na Bangus', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['bangus','garlic rice','egg','vinegar dip'], 'favorite'=>true],
        ['id'=>'sinangag_itlog', 'title'=>'Sinangag at Itlog', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['garlic rice','egg','soy sauce'], 'favorite'=>false],
        ['id'=>'ginisang_sardinas', 'title'=>'Ginisang Sardinas', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['sardines','onion','tomato','garlic'], 'favorite'=>false],
        ['id'=>'chicken_sopas', 'title'=>'Chicken Sopas', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['macaroni','chicken','milk','carrot','cabbage'], 'favorite'=>true],
        ['id'=>'beef_tapa_bowl', 'title'=>'Beef Tapa Bowl', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['beef tapa','rice','egg','cucumber'], 'favorite'=>false],
        ['id'=>'tocino_rice_bowl', 'title'=>'Tocino Rice Bowl', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['tocino','rice','egg','tomato'], 'favorite'=>false],
        ['id'=>'tinapa_fried_rice', 'title'=>'Tinapa Fried Rice', 'slot'=>'Breakfast', 'category'=>'Filipino Breakfast', 'ingredients'=>['tinapa','rice','egg','garlic','tomato'], 'favorite'=>false],
        ['id'=>'chicken_adobo', 'title'=>'Chicken Adobo', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['chicken','soy sauce','vinegar','garlic','bay leaf'], 'favorite'=>true],
        ['id'=>'pork_adobo', 'title'=>'Pork Adobo', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['pork','soy sauce','vinegar','garlic','bay leaf'], 'favorite'=>true],
        ['id'=>'bistek_tagalog', 'title'=>'Bistek Tagalog', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['beef','soy sauce','calamansi','onion','pepper'], 'favorite'=>true],
        ['id'=>'pork_menudo', 'title'=>'Pork Menudo', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['pork','potato','carrot','tomato sauce','bell pepper'], 'favorite'=>false],
        ['id'=>'chicken_afritada', 'title'=>'Chicken Afritada', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['chicken','potato','carrot','tomato sauce','bell pepper'], 'favorite'=>false],
        ['id'=>'beef_caldereta', 'title'=>'Beef Caldereta', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['beef','potato','carrot','tomato sauce','liver spread'], 'favorite'=>true],
        ['id'=>'kare_kare', 'title'=>'Kare-Kare', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['beef','peanut sauce','eggplant','string beans','bok choy'], 'favorite'=>true],
        ['id'=>'pinakbet', 'title'=>'Pinakbet', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['squash','eggplant','okra','tomato','bagoong'], 'favorite'=>false],
        ['id'=>'bicol_express', 'title'=>'Bicol Express', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['pork','coconut milk','chili','shrimp paste'], 'favorite'=>true],
        ['id'=>'laing', 'title'=>'Laing', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['taro leaves','coconut milk','chili','shrimp paste'], 'favorite'=>false],
        ['id'=>'pancit_bihon', 'title'=>'Pancit Bihon', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['bihon noodles','chicken','carrot','cabbage','soy sauce'], 'favorite'=>false],
        ['id'=>'pancit_canton', 'title'=>'Pancit Canton', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['canton noodles','pork','shrimp','vegetables','soy sauce'], 'favorite'=>false],
        ['id'=>'ginataang_kalabasa', 'title'=>'Ginataang Kalabasa', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['squash','coconut milk','string beans','shrimp'], 'favorite'=>false],
        ['id'=>'ginisang_ampalaya', 'title'=>'Ginisang Ampalaya', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['bitter melon','egg','tomato','onion'], 'favorite'=>false],
        ['id'=>'dinengdeng', 'title'=>'Dinengdeng', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['mixed vegetables','fish sauce','grilled fish'], 'favorite'=>false],
        ['id'=>'paksiw_na_isda', 'title'=>'Paksiw na Isda', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['fish','vinegar','ginger','eggplant','pepper'], 'favorite'=>false],
        ['id'=>'lechon_kawali', 'title'=>'Lechon Kawali', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['pork belly','salt','pepper','dipping sauce'], 'favorite'=>true],
        ['id'=>'pork_sisig', 'title'=>'Pork Sisig', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['pork','onion','calamansi','chili','egg'], 'favorite'=>true],
        ['id'=>'chicken_inasal', 'title'=>'Chicken Inasal', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['chicken','calamansi','lemongrass','annatto oil'], 'favorite'=>true],
        ['id'=>'embutido', 'title'=>'Embutido', 'slot'=>'Lunch', 'category'=>'Filipino Lunch', 'ingredients'=>['ground pork','egg','carrot','raisins','sausage'], 'favorite'=>false],
        ['id'=>'turon', 'title'=>'Turon', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['banana','spring roll wrapper','brown sugar','jackfruit'], 'favorite'=>true],
        ['id'=>'kwek_kwek', 'title'=>'Kwek-Kwek', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['quail eggs','flour','annatto','vinegar dip'], 'favorite'=>true],
        ['id'=>'banana_cue', 'title'=>'Banana Cue', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['saba banana','brown sugar','oil'], 'favorite'=>false],
        ['id'=>'camote_cue', 'title'=>'Camote Cue', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['sweet potato','brown sugar','oil'], 'favorite'=>false],
        ['id'=>'puto', 'title'=>'Puto', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['rice flour','sugar','coconut milk','cheese'], 'favorite'=>false],
        ['id'=>'kutsinta', 'title'=>'Kutsinta', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['rice flour','brown sugar','lye water','grated coconut'], 'favorite'=>false],
        ['id'=>'palitaw', 'title'=>'Palitaw', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['glutinous rice flour','coconut','sesame','sugar'], 'favorite'=>false],
        ['id'=>'bibingka', 'title'=>'Bibingka', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['rice flour','coconut milk','egg','salted egg','cheese'], 'favorite'=>true],
        ['id'=>'suman', 'title'=>'Suman', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['glutinous rice','coconut milk','banana leaves','sugar'], 'favorite'=>false],
        ['id'=>'ensaymada', 'title'=>'Ensaymada', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['bread','butter','sugar','cheese'], 'favorite'=>false],
        ['id'=>'hopia', 'title'=>'Hopia', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['flaky pastry','mung bean filling','ube filling'], 'favorite'=>false],
        ['id'=>'siopao_asado', 'title'=>'Siopao Asado', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['steamed bun','pork asado','sauce'], 'favorite'=>true],
        ['id'=>'cheese_sticks', 'title'=>'Cheese Sticks', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['cheese','spring roll wrapper','oil'], 'favorite'=>false],
        ['id'=>'lumpiang_togue', 'title'=>'Lumpiang Togue', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['mung bean sprouts','wrapper','carrot','tofu'], 'favorite'=>false],
        ['id'=>'halo_halo', 'title'=>'Halo-Halo', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['shaved ice','milk','sweet beans','jelly','ube'], 'favorite'=>true],
        ['id'=>'mais_con_yelo', 'title'=>'Mais con Yelo', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['corn','shaved ice','milk','sugar'], 'favorite'=>false],
        ['id'=>'ice_candy', 'title'=>'Ice Candy', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['juice','milk','sugar','ice candy bags'], 'favorite'=>false],
        ['id'=>'banana_chips', 'title'=>'Banana Chips', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['banana','sugar','oil'], 'favorite'=>false],
        ['id'=>'maruya', 'title'=>'Maruya', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['banana','flour','egg','sugar'], 'favorite'=>false],
        ['id'=>'buko_pandan', 'title'=>'Buko Pandan', 'slot'=>'Snacks', 'category'=>'Filipino Snacks', 'ingredients'=>['young coconut','pandan jelly','cream','milk'], 'favorite'=>true],
        ['id'=>'sinigang_na_baboy', 'title'=>'Sinigang na Baboy', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['pork','tamarind mix','radish','kangkong','tomato'], 'favorite'=>true],
        ['id'=>'sinigang_na_hipon', 'title'=>'Sinigang na Hipon', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['shrimp','tamarind mix','radish','okra','kangkong'], 'favorite'=>true],
        ['id'=>'tinolang_manok', 'title'=>'Tinolang Manok', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['chicken','ginger','papaya','malunggay','fish sauce'], 'favorite'=>true],
        ['id'=>'nilagang_baka', 'title'=>'Nilagang Baka', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['beef','potato','cabbage','corn','peppercorn'], 'favorite'=>false],
        ['id'=>'bulalo', 'title'=>'Bulalo', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['beef shank','corn','cabbage','potato','peppercorn'], 'favorite'=>true],
        ['id'=>'ginisang_monggo', 'title'=>'Ginisang Monggo', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['mung beans','pork','garlic','tomato','malunggay'], 'favorite'=>false],
        ['id'=>'inihaw_na_liempo', 'title'=>'Inihaw na Liempo', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['pork belly','soy sauce','calamansi','garlic','pepper'], 'favorite'=>true],
        ['id'=>'grilled_bangus', 'title'=>'Grilled Bangus', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['bangus','tomato','onion','calamansi'], 'favorite'=>true],
        ['id'=>'lumpiang_shanghai', 'title'=>'Lumpiang Shanghai', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['ground pork','carrot','spring roll wrapper','egg','garlic'], 'favorite'=>true],
        ['id'=>'ensaladang_talong', 'title'=>'Ensaladang Talong', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['eggplant','tomato','onion','vinegar','salted egg'], 'favorite'=>false],
        ['id'=>'chicken_curry', 'title'=>'Filipino Chicken Curry', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['chicken','curry powder','coconut milk','potato','carrot'], 'favorite'=>false],
        ['id'=>'paksiw_na_lechon', 'title'=>'Paksiw na Lechon', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['lechon','liver sauce','vinegar','garlic','bay leaf'], 'favorite'=>false],
        ['id'=>'relyenong_bangus', 'title'=>'Relyenong Bangus', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['bangus','carrot','peas','egg','raisins'], 'favorite'=>false],
        ['id'=>'escabeche', 'title'=>'Escabeche', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['fish','sweet sour sauce','carrot','bell pepper','ginger'], 'favorite'=>false],
        ['id'=>'ginataang_manok', 'title'=>'Ginataang Manok', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['chicken','coconut milk','papaya','ginger','chili leaves'], 'favorite'=>false],
        ['id'=>'dinuguan', 'title'=>'Dinuguan', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['pork','vinegar','garlic','chili','seasoning'], 'favorite'=>false],
        ['id'=>'pancit_molo', 'title'=>'Pancit Molo', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['wonton','chicken broth','garlic','spring onion'], 'favorite'=>true],
        ['id'=>'pochero', 'title'=>'Pochero', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['beef','banana','potato','cabbage','tomato sauce'], 'favorite'=>false],
        ['id'=>'chicken_pastel', 'title'=>'Chicken Pastel', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['chicken','cream','mushroom','carrot','potato'], 'favorite'=>false],
        ['id'=>'monggo_chicharon', 'title'=>'Monggo with Chicharon', 'slot'=>'Dinner', 'category'=>'Filipino Dinner', 'ingredients'=>['mung beans','chicharon','tomato','malunggay','garlic'], 'favorite'=>false]
    ];
    return array_map(fn($r) => [
        'id'=>$r['id'], 'title'=>$r['title'], 'slot'=>$r['slot'], 'category'=>$r['category'], 'ingredients'=>$r['ingredients'],
        'favorite'=>$r['favorite'], 'custom'=>false, 'image'=>meal_image_path($r['id']),
    ], $rows);
}
function default_weekly_meal_plan(): array
{
    return [
        'Monday'=>['Breakfast'=>'tapsilog','Lunch'=>'chicken_adobo','Snacks'=>'turon','Dinner'=>'sinigang_na_baboy'],
        'Tuesday'=>['Breakfast'=>'longsilog','Lunch'=>'pinakbet','Snacks'=>'kwek_kwek','Dinner'=>'tinolang_manok'],
        'Wednesday'=>['Breakfast'=>'champorado_tuyo','Lunch'=>'pork_menudo','Snacks'=>'puto','Dinner'=>'nilagang_baka'],
        'Thursday'=>['Breakfast'=>'pandesal_corned_beef','Lunch'=>'bicol_express','Snacks'=>'banana_cue','Dinner'=>'ginisang_monggo'],
        'Friday'=>['Breakfast'=>'arroz_caldo','Lunch'=>'kare_kare','Snacks'=>'halo_halo','Dinner'=>'inihaw_na_liempo'],
        'Saturday'=>['Breakfast'=>'tortang_talong','Lunch'=>'pancit_bihon','Snacks'=>'cheese_sticks','Dinner'=>'ensaladang_talong'],
        'Sunday'=>['Breakfast'=>'bangsilog','Lunch'=>'chicken_afritada','Snacks'=>'ice_candy','Dinner'=>'lumpiang_shanghai'],
    ];
}
function normalize_menu_library($library): array
{
    $defaults = default_menu_library();
    $byId = [];
    foreach ($defaults as $meal) $byId[$meal['id']] = $meal;
    if (is_array($library)) {
        foreach ($library as $meal) {
            if (!is_array($meal)) continue;
            $title = trim((string)($meal['title'] ?? ''));
            if ($title === '') continue;
            $id = trim((string)($meal['id'] ?? '')) ?: 'menu_' . substr(sha1($title), 0, 12);
            $id = preg_replace('/[^a-z0-9_\-]/i', '_', strtolower($id));
            $slot = normalize_meal_slot((string)($meal['slot'] ?? 'Breakfast'));
            $ingredients = $meal['ingredients'] ?? [];
            if (is_string($ingredients)) $ingredients = preg_split('/\r\n|\r|\n|,/', $ingredients);
            $ingredients = array_values(array_filter(array_map(fn($x) => trim((string)$x), is_array($ingredients) ? $ingredients : [])));
            $byId[$id] = [
                'id'=>$id, 'title'=>$title, 'slot'=>$slot,
                'category'=>trim((string)($meal['category'] ?? 'Filipino Menu')) ?: 'Filipino Menu',
                'ingredients'=>$ingredients, 'favorite'=>!empty($meal['favorite']),
                'custom'=>!empty($meal['custom']), 'image'=>trim((string)($meal['image'] ?? meal_image_path($id))) ?: meal_image_path($id),
            ];
        }
    }
    return array_values($byId);
}
function menu_index_by_id(array $library): array { $idx=[]; foreach($library as $meal) $idx[$meal['id']]=$meal; return $idx; }
function meals_by_slot(array $library, string $slot): array
{
    $slot = normalize_meal_slot($slot);
    return array_values(array_filter($library, fn($m) => normalize_meal_slot((string)($m['slot'] ?? 'Breakfast')) === $slot));
}
function normalize_weekly_meal_plan($plan, array $library): array
{
    $default = default_weekly_meal_plan();
    $ids = array_fill_keys(array_column($library, 'id'), true);
    $out = [];
    foreach (meal_days() as $day) {
        $out[$day] = [];
        foreach (meal_slots() as $slot) {
            $incoming = is_array($plan ?? null) ? (string)($plan[$day][$slot] ?? $plan[$day][rtrim($slot, 's')] ?? '') : '';
            $fallback = $default[$day][$slot] ?? '';
            $out[$day][$slot] = isset($ids[$incoming]) ? $incoming : (isset($ids[$fallback]) ? $fallback : (meals_by_slot($library, $slot)[0]['id'] ?? ''));
        }
    }
    return $out;
}
function save_weekly_meal_plan(string $hubId, array $post): void
{
    $settings = settings_for($hubId);
    $library = normalize_menu_library($settings['menu_library'] ?? []);
    $settings['meal_plan'] = normalize_weekly_meal_plan($post['meal_plan'] ?? [], $library);
    save_settings($hubId, $settings);
    flash('success', 'Weekly meal plan saved.');
}
function add_menu_item(string $hubId, array $post): void
{
    $title = trim((string)($post['menu_title'] ?? ''));
    if ($title === '') { flash('error', 'Menu title is required.'); return; }
    $slot = normalize_meal_slot((string)($post['menu_slot'] ?? 'Breakfast'));
    $baseId = preg_replace('/[^a-z0-9]+/', '_', strtolower($title));
    $id = 'custom_' . trim($baseId, '_') . '_' . substr(sha1($title . microtime(true)), 0, 6);
    $ingredients = preg_split('/\r\n|\r|\n|,/', (string)($post['menu_ingredients'] ?? '')) ?: [];
    $ingredients = array_values(array_filter(array_map(fn($x)=>trim((string)$x), $ingredients)));
    $settings = settings_for($hubId);
    $settings['menu_library'] = normalize_menu_library($settings['menu_library'] ?? []);
    $settings['menu_library'][] = [
        'id'=>$id, 'title'=>$title, 'slot'=>$slot,
        'category'=>trim((string)($post['menu_category'] ?? 'Custom Filipino Menu')) ?: 'Custom Filipino Menu',
        'ingredients'=>$ingredients, 'favorite'=>isset($post['menu_favorite']), 'custom'=>true,
        'image'=>save_uploaded_menu_image($id) ?: 'assets/meals/custom_meal.png',
    ];
    save_settings($hubId, $settings);
    flash('success', 'New menu item added.');
}
function toggle_menu_favorite(string $hubId, string $menuId): void
{
    $settings = settings_for($hubId);
    $library = normalize_menu_library($settings['menu_library'] ?? []);
    foreach ($library as $i=>$meal) {
        if (($meal['id'] ?? '') === $menuId) {
            $library[$i]['favorite'] = empty($meal['favorite']);
            $settings['menu_library'] = $library;
            save_settings($hubId, $settings);
            flash('success', !empty($library[$i]['favorite']) ? 'Added to favorite meals.' : 'Removed from favorite meals.');
            return;
        }
    }
    flash('error', 'Menu item not found.');
}
function delete_custom_menu_item(string $hubId, string $menuId): void
{
    $settings = settings_for($hubId);
    $library = normalize_menu_library($settings['menu_library'] ?? []);
    $before = count($library);
    $library = array_values(array_filter($library, fn($meal) => !(($meal['id'] ?? '') === $menuId && !empty($meal['custom']))));
    if (count($library) === $before) { flash('error', 'Only custom menu items can be deleted.'); return; }
    $settings['menu_library'] = $library;
    $settings['meal_plan'] = normalize_weekly_meal_plan($settings['meal_plan'] ?? [], $library);
    save_settings($hubId, $settings);
    flash('success', 'Custom menu item deleted.');
}
function meal_plan_grocery_items(array $settings): array
{
    $library = normalize_menu_library($settings['menu_library'] ?? []);
    $index = menu_index_by_id($library);
    $plan = normalize_weekly_meal_plan($settings['meal_plan'] ?? [], $library);
    $items = [];
    foreach ($plan as $day => $slots) foreach ($slots as $slot => $mealId) {
        $meal = $index[$mealId] ?? null;
        if (!$meal) continue;
        foreach (($meal['ingredients'] ?? []) as $ingredient) {
            $key = strtolower(trim((string)$ingredient)); if ($key === '') continue;
            if (!isset($items[$key])) $items[$key] = ['title'=>ucwords($key), 'count'=>0, 'meals'=>[]];
            $items[$key]['count']++;
            $items[$key]['meals'][] = $meal['title'];
        }
    }
    uasort($items, fn($a,$b) => $b['count'] <=> $a['count'] ?: strcmp($a['title'], $b['title']));
    return array_values($items);
}

function role_options(): array { return ['Super Admin','Admin','Member']; }
function workspace_role_options(): array { return ['Admin','Member']; }
function invite_role_options(): array { return workspace_role_options(); }
function is_super_admin_role(string $role): bool { return in_array($role, ['Super Admin','Superadmin'], true); }
function normalize_system_role(string $role): string { return is_super_admin_role($role) ? 'Super Admin' : 'User'; }
function normalize_workspace_role(string $role): string { return in_array($role, ['Admin','Owner','Parent Admin','Editor'], true) ? 'Admin' : 'Member'; }
function legacy_workspace_role(string $systemRole, string $oldRole): string { return is_super_admin_role($systemRole) || in_array($oldRole, ['Owner','Parent Admin','Editor','Admin'], true) ? 'Admin' : 'Member'; }
function role_rank(string $role): int
{
    return match($role) {
        'Super Admin', 'Superadmin' => 90,
        'Admin', 'Owner', 'Parent Admin', 'Editor' => 60,
        'Member', 'Kid View', 'Viewer' => 30,
        default => 0,
    };
}
function user_role(?array $user = null): string
{
    if (!$user) return 'Guest';
    if (is_super_admin($user)) return 'Super Admin';
    return workspace_role_for_user($user);
}
function role_at_least(?array $user, string $role): bool { return role_rank(user_role($user)) >= role_rank($role); }
function is_superadmin(?array $user): bool { return is_super_admin($user); }
function is_super_admin(?array $user): bool { return is_array($user) && is_super_admin_role((string)($user['role'] ?? '')); }
function hub_id_for_user(array $user): string { return current_workspace_id_for_user($user); }
function can_edit_items(?array $user): bool { return is_super_admin($user) || in_array(workspace_role_for_user($user), ['Admin','Member'], true); }
function can_complete_items(?array $user): bool { return can_edit_items($user); }
function can_manage_rewards(?array $user): bool { return is_super_admin($user) || workspace_role_for_user($user) === 'Admin'; }
function can_manage_family_settings(?array $user): bool { return is_super_admin($user) || workspace_role_for_user($user) === 'Admin'; }
function can_manage_access(?array $user): bool { return is_super_admin($user) || workspace_role_for_user($user) === 'Admin'; }
function can_manage_sync(?array $user): bool { return is_super_admin($user) || workspace_role_for_user($user) === 'Admin'; }
function can_manage_system(?array $user): bool { return is_super_admin($user); }
function role_badge(?array $user): string { return user_role($user); }
function permission_denied(string $message='Your current role cannot perform that action.'): void { flash('error', $message); }
function editable_role_choices_for(?array $actor): array
{
    if (can_manage_system($actor) || can_manage_access($actor)) return workspace_role_options();
    return [];
}
function role_description(string $role): string
{
    return match($role) {
        'Super Admin', 'Superadmin' => 'Full platform control: create, archive, restore, and recover workspaces, users, admins, members, and data across all private family workspaces.',
        'Admin' => 'Family hub administrator. Can manage only the active family hub, including members, Family Hub access, calendar, meals, chores, rewards, lists, settings, and sync records.',
        'Member' => 'Normal family hub user. Can use the app, add personal/family items, join other family hubs, switch family hubs, and leave a family hub when it is not their last active family hub.',
        'User' => 'Standard account that receives permissions from its active family hub membership.',
        default => 'Limited access.',
    };
}

function user_profile_photo(array $user): string
{
    return trim((string)($user['profile_photo'] ?? ''));
}
function user_profile_background(array $user): string
{
    // Personalized dashboard/card backgrounds are disabled in v6.4 to keep the UI consistent.
    return '';
}
function css_image_url(string $path): string
{
    $path = trim($path);
    if ($path === '') return '';
    return "url('" . str_replace(["'", "\\"], ["%27", "/"], $path) . "')";
}
function save_uploaded_user_media(string $fieldName, string $userId, string $kind): string
{
    if (empty($_FILES[$fieldName]) || !is_array($_FILES[$fieldName]) || (int)($_FILES[$fieldName]['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return '';
    $file = $_FILES[$fieldName];
    if ((int)($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
        flash('error', ucfirst($kind) . ' upload failed. Please choose a PNG, JPG, JPEG, or WEBP image.');
        return '';
    }
    if ((int)($file['size'] ?? 0) > 5 * 1024 * 1024) {
        flash('error', ucfirst($kind) . ' image is too large. Maximum size is 5 MB.');
        return '';
    }
    $tmp = (string)($file['tmp_name'] ?? '');
    $info = @getimagesize($tmp);
    if (!$info || empty($info['mime'])) {
        flash('error', ucfirst($kind) . ' must be a valid image file.');
        return '';
    }
    $ext = match($info['mime']) {
        'image/png' => 'png',
        'image/jpeg' => 'jpg',
        'image/webp' => 'webp',
        default => '',
    };
    if ($ext === '') {
        flash('error', ucfirst($kind) . ' supports PNG, JPG, JPEG, and WEBP only.');
        return '';
    }
    $dir = dirname(__DIR__) . '/assets/uploads/users';
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    $safeUser = preg_replace('/[^a-z0-9_\-]/i', '', $userId) ?: 'user';
    $safeKind = preg_replace('/[^a-z0-9_\-]/i', '', $kind) ?: 'image';
    $dest = $dir . '/' . $safeUser . '_' . $safeKind . '.' . $ext;
    if (!@move_uploaded_file($tmp, $dest)) {
        flash('error', 'Could not save the uploaded ' . $kind . ' image.');
        return '';
    }
    return 'assets/uploads/users/' . basename($dest);
}
function update_current_user_profile(array $actor, array $post): void
{
    $store = read_store();
    $userId = (string)($actor['id'] ?? '');
    if ($userId === '') { flash('error', 'User not found.'); return; }
    $name = trim((string)($post['display_name'] ?? $post['user_name'] ?? ''));
    if ($name !== '' && strlen($name) < 2) { flash('error', 'Display name must be at least 2 characters.'); return; }
    $photo = save_uploaded_user_media('profile_photo', $userId, 'profile');
    $updated = false;
    foreach ($store['users'] as $idx => $u) {
        if (($u['id'] ?? '') !== $userId) continue;
        if ($name !== '') { $store['users'][$idx]['name'] = $name; $updated = true; }
        if ($photo !== '') { $store['users'][$idx]['profile_photo'] = $photo; $updated = true; }
        break;
    }
    if ($updated) { write_store($store); flash('success', 'Profile updated.'); }
    else flash('info', 'No profile changes were submitted.');
}
function superadmin_update_user_identity(array $actor, array $post): void
{
    if (!can_manage_system($actor)) { permission_denied('Only Super Admin can edit platform user names and emails.'); return; }
    $targetId = (string)($post['target_user_id'] ?? '');
    $name = trim((string)($post['display_name'] ?? $post['user_name'] ?? ''));
    $email = strtolower(trim((string)($post['email'] ?? '')));
    if ($targetId === '' || $name === '' || $email === '') { flash('error', 'User, display name, and email address are required.'); return; }
    if (strlen($name) < 2) { flash('error', 'Display name must be at least 2 characters.'); return; }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { flash('error', 'Enter a valid email address.'); return; }
    $store = read_store();
    $targetFound = false;
    foreach ($store['users'] as $u) {
        if (($u['id'] ?? '') !== $targetId && strcasecmp((string)($u['email'] ?? ''), $email) === 0) {
            flash('error', 'That email address is already used by another account.');
            return;
        }
    }
    foreach ($store['users'] as $idx => $u) {
        if (($u['id'] ?? '') === $targetId) {
            $store['users'][$idx]['name'] = $name;
            $store['users'][$idx]['email'] = $email;
            $targetFound = true;
            break;
        }
    }
    if (!$targetFound) { flash('error', 'User not found.'); return; }
    write_store($store);
    flash('success', 'User name and email address updated.');
}
function superadmin_update_user_name(array $actor, array $post): void
{
    superadmin_update_user_identity($actor, $post);
}

function base32_encode_bytes(string $bytes): string
{
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $bits = '';
    for ($i = 0; $i < strlen($bytes); $i++) $bits .= str_pad(decbin(ord($bytes[$i])), 8, '0', STR_PAD_LEFT);
    $out = '';
    foreach (str_split($bits, 5) as $chunk) {
        if (strlen($chunk) < 5) $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
        $out .= $alphabet[bindec($chunk)];
    }
    return $out;
}
function base32_decode_secret(string $secret): string
{
    $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $secret = strtoupper(preg_replace('/[^A-Z2-7]/', '', $secret));
    $bits = '';
    for ($i = 0; $i < strlen($secret); $i++) {
        $pos = strpos($alphabet, $secret[$i]);
        if ($pos === false) continue;
        $bits .= str_pad(decbin($pos), 5, '0', STR_PAD_LEFT);
    }
    $bytes = '';
    foreach (str_split($bits, 8) as $chunk) if (strlen($chunk) === 8) $bytes .= chr(bindec($chunk));
    return $bytes;
}
function generate_totp_secret(): string { return base32_encode_bytes(random_bytes(20)); }
function totp_code(string $secret, ?int $time = null, int $digits = 6, int $period = 30): string
{
    $time = $time ?? time();
    $counter = intdiv($time, $period);
    $binCounter = pack('N*', 0) . pack('N*', $counter);
    $hash = hash_hmac('sha1', $binCounter, base32_decode_secret($secret), true);
    $offset = ord(substr($hash, -1)) & 0x0F;
    $truncated = ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF);
    return str_pad((string)($truncated % (10 ** $digits)), $digits, '0', STR_PAD_LEFT);
}
function verify_totp_code(string $secret, string $code, int $window = 1): bool
{
    $code = preg_replace('/\D/', '', $code);
    if (strlen($code) !== 6 || $secret === '') return false;
    for ($i = -$window; $i <= $window; $i++) if (hash_equals(totp_code($secret, time() + ($i * 30)), $code)) return true;
    return false;
}
function otpauth_uri_for_user(array $user, string $secret): string
{
    $issuer = rawurlencode(APP_NAME);
    $label = rawurlencode(APP_NAME . ':' . (string)($user['email'] ?? 'user'));
    return "otpauth://totp/{$label}?secret=" . rawurlencode($secret) . "&issuer={$issuer}&algorithm=SHA1&digits=6&period=30";
}
function app_base_url(): string
{
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
    $scheme = $https ? 'https' : 'http';
    $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $path = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? '/index.php'), '/\\');
    return $scheme . '://' . $host . ($path === '' ? '' : $path);
}
function queue_local_email(array &$store, string $to, string $subject, string $body): void
{
    $store['mail_outbox'][] = [
        'id' => uid('mail_'),
        'to' => $to,
        'subject' => $subject,
        'body' => $body,
        'created_at' => app_now()->format(DateTimeInterface::ATOM),
    ];
    @mail($to, $subject, $body, 'From: noreply@homesynchub.local');
}
function request_password_reset(array $post): void
{
    $email = trim((string)($post['email'] ?? ''));
    $store = read_store();
    $user = filter_var($email, FILTER_VALIDATE_EMAIL) ? user_by_email($store, $email) : null;
    unset($_SESSION['recovery_options']);
    if ($user && ($user['status'] ?? 'active') === 'active') {
        $token = bin2hex(random_bytes(32));
        $store['password_reset_tokens'][] = [
            'id' => uid('reset_'),
            'user_id' => (string)$user['id'],
            'token_hash' => hash('sha256', $token),
            'expires_at' => app_now()->modify('+1 hour')->format(DateTimeInterface::ATOM),
            'used_at' => '',
            'created_at' => app_now()->format(DateTimeInterface::ATOM),
        ];
        $link = app_base_url() . '/index.php?page=login&reset_token=' . rawurlencode($token);
        queue_local_email($store, (string)$user['email'], 'HomeSyncHub password reset', "Use this password reset link within 1 hour:\n\n" . $link . "\n\nIf you did not request this, ignore this message.");
        write_store($store);
        $_SESSION['recovery_options'] = [
            'email' => (string)$user['email'],
            'masked_email' => mask_email_address((string)$user['email']),
            'reset_link' => $link,
            'expires_at' => app_now()->modify('+1 hour')->format(DateTimeInterface::ATOM),
        ];
        flash('success', 'Recovery options are ready for the registered email.');
        return;
    }
    flash('error', 'No active account was found for that registered email.');
}
function reset_password_with_token(array $post): void
{
    $token = (string)($post['reset_token'] ?? '');
    $password = (string)($post['password'] ?? '');
    $confirm = (string)($post['password_confirm'] ?? '');
    if (strlen($password) < 8 || $password !== $confirm) { flash('error', 'New password must be at least 8 characters and must match confirmation.'); return; }
    $store = read_store();
    $hash = hash('sha256', $token);
    $now = app_now();
    foreach (($store['password_reset_tokens'] ?? []) as $idx => $row) {
        $expires = parse_dt($row['expires_at'] ?? '');
        if (($row['token_hash'] ?? '') === $hash && empty($row['used_at']) && $expires && $expires >= $now) {
            $target = (string)($row['user_id'] ?? '');
            foreach ($store['users'] as $uIdx => $u) {
                if (($u['id'] ?? '') === $target) {
                    $store['users'][$uIdx]['password_hash'] = password_hash($password, PASSWORD_DEFAULT);
                    $store['password_reset_tokens'][$idx]['used_at'] = app_now()->format(DateTimeInterface::ATOM);
                    $store['remember_login_tokens'] = array_values(array_filter($store['remember_login_tokens'] ?? [], fn($row) => is_array($row) && (($row['user_id'] ?? '') !== $target)));
                    write_store($store);
                    unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_started_at'], $_SESSION['recovery_options']);
                    flash('success', 'Password reset successfully. You can now log in.');
                    return;
                }
            }
        }
    }
    flash('error', 'Password reset link is invalid, expired, or already used.');
}
function update_current_user_password(array $actor, array $post): void
{
    $current = (string)($post['current_password'] ?? '');
    $new = (string)($post['new_password'] ?? '');
    $confirm = (string)($post['new_password_confirm'] ?? '');
    if (!password_verify($current, $actor['password_hash'] ?? '')) { flash('error', 'Current password is incorrect.'); return; }
    if (strlen($new) < 8 || $new !== $confirm) { flash('error', 'New password must be at least 8 characters and must match confirmation.'); return; }
    $store = read_store();
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === ($actor['id'] ?? '')) { $store['users'][$idx]['password_hash'] = password_hash($new, PASSWORD_DEFAULT); write_store($store); flash('success', 'Password changed successfully.'); return; }
    flash('error', 'User account was not found.');
}
function start_2fa_setup(array $actor): void
{
    $store = read_store();
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === ($actor['id'] ?? '')) { $store['users'][$idx]['two_factor_pending_secret'] = generate_totp_secret(); write_store($store); flash('success', '2FA setup key generated. Add it to Google Authenticator, Microsoft Authenticator, or a compatible TOTP app, then confirm the 6-digit code.'); return; }
}
function confirm_2fa_setup(array $actor, array $post): void
{
    $store = read_store();
    foreach ($store['users'] as $idx => $u) {
        if (($u['id'] ?? '') !== ($actor['id'] ?? '')) continue;
        $secret = (string)($u['two_factor_pending_secret'] ?? '');
        if ($secret === '' || !verify_totp_code($secret, (string)($post['totp_code'] ?? ''))) { flash('error', 'Invalid 2FA code. Check the setup key and your phone time.'); return; }
        $store['users'][$idx]['two_factor_secret'] = $secret;
        $store['users'][$idx]['two_factor_enabled'] = true;
        $store['users'][$idx]['two_factor_pending_secret'] = '';
        write_store($store); flash('success', 'Two-factor authentication is now enabled.'); return;
    }
}
function disable_2fa(array $actor, array $post): void
{
    if (!password_verify((string)($post['password'] ?? ''), $actor['password_hash'] ?? '')) { flash('error', 'Password is incorrect.'); return; }
    if (!verify_totp_code((string)($actor['two_factor_secret'] ?? ''), (string)($post['totp_code'] ?? ''))) { flash('error', 'Invalid 2FA code.'); return; }
    $store = read_store();
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === ($actor['id'] ?? '')) { $store['users'][$idx]['two_factor_enabled'] = false; $store['users'][$idx]['two_factor_secret'] = ''; $store['users'][$idx]['two_factor_pending_secret'] = ''; write_store($store); flash('success', 'Two-factor authentication has been disabled.'); return; }
}
function notification_items_for(string $hubId, int $limit=8): array
{
    $now = app_now();
    $latest = $now->modify('+14 days');
    $items = array_values(array_filter(items_for($hubId), function($item) use ($now, $latest) {
        if (($item['status'] ?? 'pending') === 'done') return false;
        $dt = parse_dt($item['start'] ?? '');
        if (!$dt) return false;
        return $dt >= $now && $dt <= $latest;
    }));
    usort($items, fn($a,$b)=>strcmp((string)($a['start'] ?? ''), (string)($b['start'] ?? '')));
    return array_slice($items, 0, $limit);
}
function imminent_notification_count(array $items, int $minutes = 30): int
{
    $now = app_now(); $next = $now->modify('+' . max(1, $minutes) . ' minutes'); $count = 0;
    foreach ($items as $item) {
        $dt = parse_dt($item['start'] ?? '');
        if ($dt && $dt >= $now && $dt <= $next) $count++;
    }
    return $count;
}

function normalize_join_code(string $code): string
{
    $code = strtoupper(preg_replace('/[^A-Z0-9]/', '', $code));
    return strlen($code) >= 6 ? substr($code, 0, 12) : '';
}
function generate_join_code(array $store): string
{
    $used = [];
    foreach (($store['workspaces'] ?? []) as $ws) $used[strtoupper((string)($ws['join_code'] ?? ''))] = true;
    foreach (($store['share_invites'] ?? []) as $inv) $used[strtoupper((string)($inv['code'] ?? ''))] = true;
    do { $code = strtoupper(bin2hex(random_bytes(3))); } while (isset($used[$code]));
    return $code;
}
function membership_exists(array $store, string $workspaceId, string $userId): bool
{
    foreach (($store['workspace_members'] ?? []) as $m) if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === $userId) return true;
    return false;
}
function workspace_by_id(array $store, string $workspaceId): ?array
{
    foreach (($store['workspaces'] ?? []) as $ws) if (($ws['id'] ?? '') === $workspaceId) return $ws;
    return null;
}
function workspace_name(string $workspaceId): string
{
    $ws = workspace_by_id(read_store(), $workspaceId);
    return $ws ? (string)($ws['name'] ?? 'Workspace') : 'Workspace';
}
function current_workspace_id_for_user(array $user): string
{
    $store = read_store();
    $sessionWs = (string)($_SESSION['active_workspace_id'] ?? '');
    if ($sessionWs !== '' && user_can_access_workspace($user, $sessionWs, $store)) return $sessionWs;
    $userWs = (string)($user['active_workspace_id'] ?? '');
    if ($userWs !== '' && user_can_access_workspace($user, $userWs, $store)) { $_SESSION['active_workspace_id'] = $userWs; return $userWs; }
    $workspaces = accessible_workspaces_for_user($user, $store, false);
    $first = $workspaces[0]['id'] ?? '';
    if ($first !== '') $_SESSION['active_workspace_id'] = $first;
    return (string)$first;
}
function user_can_access_workspace(array $user, string $workspaceId, ?array $store=null): bool
{
    $store ??= read_store();
    $ws = workspace_by_id($store, $workspaceId);
    if (!$ws || ($ws['status'] ?? 'active') !== 'active') return false;
    if (is_super_admin($user)) return true;
    foreach (($store['workspace_members'] ?? []) as $m) {
        if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === ($user['id'] ?? '') && ($m['status'] ?? 'active') === 'active') return true;
    }
    return false;
}
function accessible_workspaces_for_user(array $user, ?array $store=null, bool $includeArchived=false): array
{
    $store ??= read_store();
    $out = [];
    if (is_super_admin($user)) {
        foreach (($store['workspaces'] ?? []) as $ws) if ($includeArchived || ($ws['status'] ?? 'active') === 'active') $out[] = $ws;
        usort($out, fn($a,$b)=>strcmp((string)($a['name']??''),(string)($b['name']??'')));
        return $out;
    }
    foreach (($store['workspace_members'] ?? []) as $m) {
        if (($m['user_id'] ?? '') !== ($user['id'] ?? '') || ($m['status'] ?? 'active') !== 'active') continue;
        $ws = workspace_by_id($store, (string)($m['workspace_id'] ?? ''));
        if ($ws && ($includeArchived || ($ws['status'] ?? 'active') === 'active')) $out[] = $ws;
    }
    usort($out, fn($a,$b)=>strcmp((string)($a['name']??''),(string)($b['name']??'')));
    return $out;
}

function available_family_hubs_for_join(?array $store=null, ?array $user=null): array
{
    $store ??= read_store();
    $linked = [];
    if ($user) {
        foreach (($store['workspace_members'] ?? []) as $m) {
            if (($m['user_id'] ?? '') === ($user['id'] ?? '') && ($m['status'] ?? 'active') === 'active') {
                $linked[(string)($m['workspace_id'] ?? '')] = true;
            }
        }
    }
    $out = [];
    foreach (($store['workspaces'] ?? []) as $ws) {
        $workspaceId = (string)($ws['id'] ?? '');
        if ($workspaceId === '' || $workspaceId === 'workspace_system') continue;
        if (($ws['status'] ?? 'active') !== 'active') continue;
        if ($user && isset($linked[$workspaceId])) continue;
        $out[] = $ws;
    }
    usort($out, fn($a,$b)=>strcmp((string)($a['name']??''),(string)($b['name']??'')));
    return $out;
}

function find_active_workspace_by_id(array $store, string $workspaceId): ?array
{
    $workspaceId = trim($workspaceId);
    if ($workspaceId === '') return null;
    foreach (($store['workspaces'] ?? []) as $ws) {
        if ((string)($ws['id'] ?? '') === $workspaceId && ($ws['status'] ?? 'active') === 'active') return $ws;
    }
    return null;
}

function workspace_members_for(string $workspaceId, bool $includeArchived=false): array
{
    $store = read_store(); $usersById=[]; foreach ($store['users'] as $u) $usersById[$u['id']]=$u;
    $out=[];
    foreach (($store['workspace_members'] ?? []) as $m) {
        if (($m['workspace_id'] ?? '') !== $workspaceId) continue;
        if (!$includeArchived && ($m['status'] ?? 'active') !== 'active') continue;
        $u = $usersById[$m['user_id'] ?? ''] ?? [];
        $out[] = array_merge($m, ['user_name'=>$u['name'] ?? 'Unknown User', 'email'=>$u['email'] ?? '', 'user_status'=>$u['status'] ?? 'active']);
    }
    usort($out, fn($a,$b)=>strcmp((string)($a['user_name']??''),(string)($b['user_name']??'')));
    return $out;
}
function current_membership_for_user(array $user, ?string $workspaceId=null): ?array
{
    $workspaceId = $workspaceId ?: current_workspace_id_for_user($user);
    foreach ((read_store()['workspace_members'] ?? []) as $m) {
        if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === ($user['id'] ?? '') && ($m['status'] ?? 'active') === 'active') return $m;
    }
    return null;
}
function workspace_role_for_user(?array $user, ?string $workspaceId=null): string
{
    if (!$user) return 'Guest';
    if (is_super_admin($user)) return 'Super Admin';
    $m = current_membership_for_user($user, $workspaceId);
    return normalize_workspace_role((string)($m['role'] ?? 'Member'));
}
function active_workspace_count_for_user(string $userId): int
{
    $store=read_store(); $count=0;
    foreach (($store['workspace_members'] ?? []) as $m) {
        if (($m['user_id'] ?? '') === $userId && ($m['status'] ?? 'active') === 'active') {
            $ws = workspace_by_id($store, (string)($m['workspace_id'] ?? ''));
            if ($ws && ($ws['status'] ?? 'active') === 'active') $count++;
        }
    }
    return $count;
}
function users_for_admin(): array { return read_store()['users'] ?? []; }
function workspaces_for_admin(bool $includeArchived=true): array
{
    $store=read_store(); $out=[];
    foreach (($store['workspaces'] ?? []) as $ws) {
        if (!$includeArchived && ($ws['status'] ?? 'active') !== 'active') continue;
        $members = workspace_members_for((string)($ws['id'] ?? ''), true);
        $activeMembers = array_values(array_filter($members, fn($m)=>($m['status'] ?? '')==='active'));
        $out[] = array_merge($ws, ['member_count'=>count($activeMembers), 'total_member_count'=>count($members)]);
    }
    usort($out, fn($a,$b)=>strcmp((string)($a['name']??''),(string)($b['name']??'')));
    return $out;
}
function family_label_for_user(array $user): string { return workspace_name(current_workspace_id_for_user($user)); }

function find_invite_by_code(array $store, string $code): ?array
{
    $code = strtoupper(trim($code));
    if ($code === '') return null;
    foreach ($store['share_invites'] as $invite) {
        if (strtoupper((string)($invite['code'] ?? '')) === $code && ($invite['status'] ?? 'active') === 'active') return $invite;
    }
    return null;
}

function seed_demo_items(string $userId): void
{
    $store = read_store();
    foreach ($store['items'] as $item) if (($item['user_id'] ?? '') === $userId) return;
    $today = app_now(); $tomorrow = $today->modify('+1 day');
    $sample = [
        ['type'=>'event','title'=>'Family planning sync','member'=>'Family','category'=>'Home','start'=>$today->setTime(18,30)->format('Y-m-d\TH:i'),'end'=>$today->setTime(19,0)->format('Y-m-d\TH:i'),'location'=>'Dining table','notes'=>'Review calendar, chores, meals, lists, and rewards.','points'=>0,'stars'=>0,'repeat'=>'weekly','reminder_minutes'=>15,'invitees'=>'','sync_provider'=>'HomeSync','sync_mode'=>'local'],
        ['type'=>'chore','title'=>'Clean dining area','member'=>'Kid','category'=>'Chore','start'=>$today->setTime(17,0)->format('Y-m-d\TH:i'),'end'=>'','location'=>'Home','notes'=>'Wipe the table and put things back.','points'=>10,'stars'=>2,'repeat'=>'daily','reminder_minutes'=>15,'invitees'=>'','sync_provider'=>'HomeSync','sync_mode'=>'local'],
        ['type'=>'task','title'=>'Pay utility bill','member'=>'Parent','category'=>'Finance','start'=>$tomorrow->setTime(10,0)->format('Y-m-d\TH:i'),'end'=>'','location'=>'','notes'=>'Check due amount before payment.','points'=>15,'stars'=>1,'repeat'=>'none','reminder_minutes'=>30,'invitees'=>'','sync_provider'=>'HomeSync','sync_mode'=>'local'],
        ['type'=>'meal','title'=>'Chicken pasta','member'=>'Family','category'=>'Dinner','meal_slot'=>'Dinner','start'=>$today->setTime(19,0)->format('Y-m-d\TH:i'),'end'=>'','location'=>'Kitchen','notes'=>"pasta\nchicken\nsalad",'points'=>10,'stars'=>1,'repeat'=>'none','reminder_minutes'=>15,'invitees'=>'','sync_provider'=>'HomeSync','sync_mode'=>'local'],
        ['type'=>'event','title'=>'School activity','member'=>'Kid','category'=>'School','start'=>$tomorrow->setTime(15,30)->format('Y-m-d\TH:i'),'end'=>$tomorrow->setTime(17,0)->format('Y-m-d\TH:i'),'location'=>'School Hall','notes'=>'Bring water bottle.','points'=>0,'stars'=>0,'repeat'=>'none','reminder_minutes'=>45,'invitees'=>'','sync_provider'=>'Google-ready','sync_mode'=>'two-way-ready'],
    ];
    foreach ($sample as $item) {
        $item['id'] = uid('item_'); $item['user_id'] = $userId; $item['status'] = 'pending';
        $item['meal_slot'] = $item['meal_slot'] ?? ''; $item['created_at'] = app_now()->format(DateTimeInterface::ATOM); $item['completed_at'] = '';
        $store['items'][] = $item;
    }
    $listSamples = [
        ['list_type'=>'grocery','title'=>'Milk','notes'=>'2 cartons'],
        ['list_type'=>'grocery','title'=>'Eggs','notes'=>'Breakfast prep'],
        ['list_type'=>'todo','title'=>'Call clinic for appointment','notes'=>'Ask for weekend slot'],
        ['list_type'=>'reminder','title'=>'Bring umbrella','notes'=>'Rainy-week reminder'],
    ];
    foreach ($listSamples as $li) { $li['id']=uid('list_'); $li['user_id']=$userId; $li['status']='open'; $li['created_at']=app_now()->format(DateTimeInterface::ATOM); $store['list_items'][]=$li; }
    $store['photos'][] = ['id'=>uid('photo_'), 'user_id'=>$userId, 'title'=>'Family moments', 'caption'=>'Family board memory card', 'color'=>'#49d9ff', 'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    write_store($store);
}

function items_for(string $userId, ?string $type = null, bool $includeDone = true): array
{
    $items = array_values(array_filter(read_store()['items'], function($item) use ($userId,$type,$includeDone) {
        if (($item['user_id'] ?? '') !== $userId) return false;
        if ($type && ($item['type'] ?? '') !== $type) return false;
        if (!$includeDone && ($item['status'] ?? 'pending') === 'done') return false;
        return true;
    }));
    usort($items, fn($a,$b) => (($a['start'] ?? '') === ($b['start'] ?? '')) ? strcmp($a['title'] ?? '', $b['title'] ?? '') : strcmp(($a['start'] ?? '9999'), ($b['start'] ?? '9999')));
    return $items;
}
function list_items_for(string $userId, ?string $type = null): array
{
    $items = array_values(array_filter(read_store()['list_items'], fn($item) => (($item['user_id'] ?? '') === $userId) && (!$type || ($item['list_type'] ?? '') === $type)));
    usort($items, fn($a,$b) => strcmp($a['status'] ?? 'open', $b['status'] ?? 'open') ?: strcmp($b['created_at'] ?? '', $a['created_at'] ?? ''));
    return $items;
}
function activity_for(string $userId): array { $a = array_values(array_filter(read_store()['activity'], fn($x)=>($x['user_id'] ?? '') === $userId)); usort($a, fn($x,$y)=>strcmp($y['created_at'] ?? '', $x['created_at'] ?? '')); return $a; }
function connections_for(string $userId): array { return array_values(array_filter(read_store()['connections'], fn($x)=>($x['user_id'] ?? '') === $userId)); }
function shares_for(string $userId): array { return array_values(array_filter(read_store()['share_invites'], fn($x)=>($x['user_id'] ?? '') === $userId)); }
function photos_for(string $userId): array { return array_values(array_filter(read_store()['photos'], fn($x)=>($x['user_id'] ?? '') === $userId)); }


function inferred_proof_required(string $type, string $title, string $category = '', string $notes = ''): bool
{
    $haystack = strtolower($type . ' ' . $title . ' ' . $category . ' ' . $notes);
    if ($type !== 'task') return false;
    return (bool)preg_match('/\b(homework|worksheet|assignment|spelling|maths?|science|english|school work|schoolwork)\b/', $haystack);
}

function item_requires_proof(array $item): bool
{
    if (($item['status'] ?? 'pending') === 'done') return false;
    if (!empty($item['proof_required'])) return true;
    return inferred_proof_required((string)($item['type'] ?? 'task'), (string)($item['title'] ?? ''), (string)($item['category'] ?? ''), (string)($item['notes'] ?? ''));
}

function upload_root_dir(): string { return __DIR__ . '/../assets/uploads'; }
function upload_public_path(string $subdir, string $filename): string { return 'assets/uploads/' . trim($subdir, '/') . '/' . $filename; }

function ensure_upload_subdir(string $subdir): string
{
    $dir = upload_root_dir() . '/' . trim($subdir, '/');
    if (!is_dir($dir)) mkdir($dir, 0775, true);
    $htaccess = $dir . '/.htaccess';
    if (!file_exists($htaccess)) {
        file_put_contents($htaccess, "Options -Indexes\n<FilesMatch \"\\.(php|phtml|phar|cgi|pl|py|sh)$\">\nRequire all denied\n</FilesMatch>\n");
    }
    return $dir;
}

function normalize_file_array(string $field): array
{
    if (empty($_FILES[$field])) return [];
    $f = $_FILES[$field];
    if (is_array($f['name'])) {
        $out = [];
        foreach ($f['name'] as $i => $name) {
            $out[] = [
                'name' => $name,
                'type' => $f['type'][$i] ?? '',
                'tmp_name' => $f['tmp_name'][$i] ?? '',
                'error' => $f['error'][$i] ?? UPLOAD_ERR_NO_FILE,
                'size' => $f['size'][$i] ?? 0,
            ];
        }
        return $out;
    }
    return [[
        'name' => $f['name'] ?? '', 'type' => $f['type'] ?? '', 'tmp_name' => $f['tmp_name'] ?? '',
        'error' => $f['error'] ?? UPLOAD_ERR_NO_FILE, 'size' => $f['size'] ?? 0,
    ]];
}

function save_uploaded_file_record(string $field, string $subdir, string $prefix, array $allowedExt = ['jpg','jpeg','png','webp','heic','pdf']): ?array
{
    $files = normalize_file_array($field);
    $file = $files[0] ?? null;
    if (!$file || (int)$file['error'] === UPLOAD_ERR_NO_FILE) return null;
    if ((int)$file['error'] !== UPLOAD_ERR_OK) return ['error' => 'Upload failed. Please try again.'];
    $size = (int)($file['size'] ?? 0);
    if ($size <= 0 || $size > 8 * 1024 * 1024) return ['error' => 'Upload must be 8 MB or smaller.'];
    $original = basename((string)($file['name'] ?? 'upload'));
    $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
    if (!in_array($ext, $allowedExt, true)) return ['error' => 'Unsupported file type. Please upload an image or PDF.'];
    $safePrefix = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $prefix) ?: 'upload';
    $filename = $safePrefix . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
    $dir = ensure_upload_subdir($subdir);
    $dest = $dir . '/' . $filename;
    if (!move_uploaded_file((string)$file['tmp_name'], $dest)) return ['error' => 'The uploaded file could not be saved.'];
    return [
        'filename' => $filename,
        'original_name' => $original,
        'path' => upload_public_path($subdir, $filename),
        'absolute_path' => $dest,
        'mime' => mime_content_type($dest) ?: ((string)($file['type'] ?? 'application/octet-stream')),
        'size' => $size,
    ];
}

function save_proof_upload(string $hubId, string $itemId, string $actorUserId, string $field = 'proof_file'): array
{
    $saved = save_uploaded_file_record($field, 'proofs', 'proof_' . substr($itemId, -8));
    if (!$saved) return ['error' => 'Please upload a homework photo or file before parent review.'];
    if (isset($saved['error'])) return $saved;
    $store = read_store();
    $record = [
        'id' => uid('proof_'), 'user_id' => $hubId, 'item_id' => $itemId, 'actor_id' => $actorUserId,
        'path' => $saved['path'], 'original_name' => $saved['original_name'], 'mime' => $saved['mime'], 'size' => $saved['size'],
        'status' => 'uploaded', 'created_at' => app_now()->format(DateTimeInterface::ATOM),
    ];
    $store['proof_uploads'][] = $record;
    foreach ($store['items'] as $idx => $item) {
        if (($item['id'] ?? '') === $itemId && ($item['user_id'] ?? '') === $hubId) {
            $store['items'][$idx]['proof_upload_id'] = $record['id'];
            $store['items'][$idx]['proof_status'] = 'uploaded';
            $store['items'][$idx]['verification_message'] = 'Homework uploaded. Waiting for parent review.';
        }
    }
    write_store($store);
    $record['absolute_path'] = $saved['absolute_path'];
    return $record;
}

function proof_upload_by_id(string $proofId): ?array
{
    foreach ((read_store()['proof_uploads'] ?? []) as $proof) if (($proof['id'] ?? '') === $proofId) return $proof;
    return null;
}

function prepare_homework_parent_review_result(array $item, array $proof): array
{
    return [
        'status' => 'pending_parent_review',
        'correct' => false,
        'message' => 'Homework uploaded. Waiting for parent review.',
        'confidence' => null,
    ];
}

function update_item_proof_result(string $hubId, string $itemId, string $proofId, array $result): void
{
    $store = read_store();
    foreach ($store['proof_uploads'] as $idx => $proof) {
        if (($proof['id'] ?? '') === $proofId) {
            $store['proof_uploads'][$idx]['status'] = (string)($result['status'] ?? 'needs_review');
            $store['proof_uploads'][$idx]['verification_message'] = (string)($result['message'] ?? '');
            $store['proof_uploads'][$idx]['verified_at'] = app_now()->format(DateTimeInterface::ATOM);
        }
    }
    foreach ($store['items'] as $idx => $item) {
        if (($item['id'] ?? '') === $itemId && ($item['user_id'] ?? '') === $hubId) {
            $store['items'][$idx]['proof_upload_id'] = $proofId;
            $store['items'][$idx]['proof_status'] = (string)($result['status'] ?? 'needs_review');
            $store['items'][$idx]['verification_message'] = (string)($result['message'] ?? '');
        }
    }
    write_store($store);
}

function create_device_unlock_record(array &$store, string $hubId, string $actorUserId, array $item): ?array
{
    $settings = settings_for($hubId);
    if (empty($settings['device_unlock_enabled'])) return null;
    $minutes = max(5, min(1440, (int)($settings['device_unlock_minutes'] ?? 120)));
    $now = app_now();
    $record = [
        'id' => uid('unlock_'), 'user_id' => $hubId, 'actor_id' => $actorUserId, 'item_id' => (string)($item['id'] ?? ''),
        'member' => (string)($item['member'] ?? 'Family'), 'device' => trim((string)($settings['device_name'] ?? 'Family device')) ?: 'Family device',
        'platform' => 'adapter', 'status' => 'granted', 'minutes' => $minutes,
        'created_at' => $now->format(DateTimeInterface::ATOM), 'expires_at' => $now->modify('+' . $minutes . ' minutes')->format(DateTimeInterface::ATOM),
        'webhook_sent' => false, 'webhook_status' => '',
    ];
    $store['device_unlocks'][] = $record;
    return $record;
}

function send_device_unlock_webhook(array $unlock, string $hubId): array
{
    $settings = settings_for($hubId);
    $url = trim((string)($settings['device_unlock_webhook_url'] ?? ''));
    if ($url === '') return ['sent'=>false, 'status'=>'No webhook configured. Unlock grant recorded locally.'];
    if (!preg_match('/^https:\/\//i', $url)) return ['sent'=>false, 'status'=>'Webhook must use HTTPS. Unlock grant recorded locally only.'];
    if (!function_exists('curl_init')) return ['sent'=>false, 'status'=>'PHP cURL extension is not enabled. Unlock grant recorded locally only.'];
    $payload = json_encode(['event'=>'device_unlock_granted', 'unlock'=>$unlock], JSON_UNESCAPED_SLASHES);
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_HTTPHEADER=>['Content-Type: application/json'], CURLOPT_POSTFIELDS=>$payload, CURLOPT_TIMEOUT=>6]);
    $raw = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $err = curl_error($ch);
    curl_close($ch);
    return ['sent'=>$code >= 200 && $code < 300, 'status'=>($code >= 200 && $code < 300) ? 'Webhook delivered.' : ('Webhook failed: ' . ($err ?: 'HTTP ' . $code))];
}

function update_device_unlock_webhook_status(string $unlockId, string $hubId, array $result): void
{
    $store = read_store();
    foreach ($store['device_unlocks'] as $idx => $unlock) {
        if (($unlock['id'] ?? '') === $unlockId && ($unlock['user_id'] ?? '') === $hubId) {
            $store['device_unlocks'][$idx]['webhook_sent'] = !empty($result['sent']);
            $store['device_unlocks'][$idx]['webhook_status'] = (string)($result['status'] ?? '');
        }
    }
    write_store($store);
}

function device_unlocks_for(string $hubId, int $limit = 5): array
{
    $items = array_values(array_filter(read_store()['device_unlocks'] ?? [], fn($x) => ($x['user_id'] ?? '') === $hubId));
    usort($items, fn($a,$b)=>strcmp((string)($b['created_at'] ?? ''), (string)($a['created_at'] ?? '')));
    return array_slice($items, 0, $limit);
}

function find_item_by_id(string $hubId, string $itemId): ?array
{
    foreach ((read_store()['items'] ?? []) as $item) if (($item['user_id'] ?? '') === $hubId && ($item['id'] ?? '') === $itemId) return $item;
    return null;
}

function first_pending_homework_item(string $hubId, string $message = ''): ?array
{
    $items = array_values(array_filter(items_for($hubId, 'task', false), fn($item) => item_requires_proof($item)));
    if (!$items) return null;
    $msg = strtolower($message);
    if ($msg !== '') {
        foreach ($items as $item) {
            $member = strtolower((string)($item['member'] ?? ''));
            if ($member !== '' && str_contains($msg, $member)) return $item;
        }
    }
    return $items[0];
}

function complete_item_record(string $actorUserId, string $hubId, string $itemId, string $reason = 'Completed'): array
{
    $store = read_store(); $today = app_now()->format('Y-m-d');
    foreach ($store['items'] as $i=>$item) {
        if (($item['id'] ?? '') !== $itemId || ($item['user_id'] ?? '') !== $hubId) continue;
        if (($item['status'] ?? 'pending') === 'done') return ['ok'=>false, 'message'=>'This item is already complete.'];
        $points = (int)($item['points'] ?? 0); $stars = (int)($item['stars'] ?? 0);
        $unlock = create_device_unlock_record($store, $hubId, $actorUserId, $item);
        $store['items'][$i]['status'] = 'done';
        $store['items'][$i]['completed_at'] = app_now()->format(DateTimeInterface::ATOM);
        if ($unlock) $store['items'][$i]['device_unlock_id'] = $unlock['id'];
        foreach ($store['users'] as $u=>$user) if (($user['id'] ?? '') === $actorUserId) {
            $last = $user['last_completed_date'] ?? ''; $streak = (int)($user['streak'] ?? 0); $yesterday = app_now()->modify('-1 day')->format('Y-m-d');
            if ($last === $today) { } elseif ($last === $yesterday) $streak++; else $streak = 1;
            $store['users'][$u]['points'] = (int)($user['points'] ?? 0) + $points;
            $store['users'][$u]['stars'] = (int)($user['stars'] ?? 0) + $stars;
            $store['users'][$u]['streak'] = $streak; $store['users'][$u]['last_completed_date'] = $today; break;
        }
        $store['activity'][] = ['id'=>uid('act_'), 'user_id'=>$hubId, 'actor_id'=>$actorUserId, 'item_id'=>$itemId, 'title'=>$item['title'] ?? 'Completed item', 'type'=>$item['type'] ?? 'task', 'member'=>$item['member'] ?? 'Family', 'points'=>$points, 'stars'=>$stars, 'created_at'=>app_now()->format(DateTimeInterface::ATOM), 'completion_reason'=>$reason, 'device_unlock_id'=>$unlock['id'] ?? '', 'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
        write_store($store);
        if ($unlock) {
            $webhook = send_device_unlock_webhook($unlock, $hubId);
            update_device_unlock_webhook_status($unlock['id'], $hubId, $webhook);
        }
        $msg = 'Completed. +' . $points . ' points and +' . $stars . ' stars earned.';
        if ($unlock) $msg .= ' Device access granted for ' . (int)$unlock['minutes'] . ' minutes.';
        return ['ok'=>true, 'message'=>$msg, 'item'=>$item, 'unlock'=>$unlock, 'points'=>$points, 'stars'=>$stars];
    }
    return ['ok'=>false, 'message'=>'Item not found.'];
}

function submit_homework_proof_for_parent_review(array $actor, string $hubId, string $itemId, string $field = 'proof_file'): array
{
    $item = find_item_by_id($hubId, $itemId);
    if (!$item) return ['ok'=>false, 'message'=>'Item not found.'];
    if (!item_requires_proof($item)) return complete_item_record((string)$actor['id'], $hubId, $itemId, 'No proof required');
    $proof = save_proof_upload($hubId, $itemId, (string)$actor['id'], $field);
    if (isset($proof['error'])) return ['ok'=>false, 'message'=>$proof['error']];
    $result = prepare_homework_parent_review_result($item, $proof);
    update_item_proof_result($hubId, $itemId, (string)$proof['id'], $result);
    return [
        'ok' => true,
        'message' => 'Homework uploaded. Waiting for parent review. A parent or Family Hub Admin must approve it before rewards are released.',
        'proof' => $proof,
        'verification' => $result,
    ];
}

function save_chat_uploads(string $hubId, string $actorUserId, string $message, string $field = 'chat_files'): array
{
    $records = [];
    foreach (normalize_file_array($field) as $idx => $file) {
        $_FILES['_single_chat_file'] = $file;
        $saved = save_uploaded_file_record('_single_chat_file', 'chat', 'chat_' . substr($actorUserId, -6), ['jpg','jpeg','png','webp','heic','pdf','txt','doc','docx']);
        unset($_FILES['_single_chat_file']);
        if (!$saved || isset($saved['error'])) continue;
        $records[] = ['id'=>uid('chatfile_'), 'user_id'=>$hubId, 'actor_id'=>$actorUserId, 'message'=>$message, 'path'=>$saved['path'], 'original_name'=>$saved['original_name'], 'mime'=>$saved['mime'], 'size'=>$saved['size'], 'created_at'=>app_now()->format(DateTimeInterface::ATOM)];
    }
    if ($records) {
        $store = read_store();
        foreach ($records as $record) $store['chat_uploads'][] = $record;
        write_store($store);
    }
    return $records;
}


function find_pending_item_for_completion(string $hubId, string $message): ?array
{
    $messageLower = strtolower($message);
    $items = array_values(array_filter(items_for($hubId, null, false), fn($item) => ($item['status'] ?? 'pending') !== 'done'));
    if (!$items) return null;
    $best = null; $bestScore = 0;
    foreach ($items as $item) {
        $title = strtolower((string)($item['title'] ?? ''));
        $type = strtolower((string)($item['type'] ?? 'task'));
        $member = strtolower((string)($item['member'] ?? ''));
        $score = 0;
        if ($type === 'chore' && preg_match('/\b(chore|dish|dishes|wash|clean|trash|laundry|mop|sweep)\b/', $messageLower)) $score += 4;
        if ($type === 'task' && preg_match('/\b(task|homework|assignment|worksheet)\b/', $messageLower)) $score += 3;
        if ($member !== '' && str_contains($messageLower, $member)) $score += 2;
        foreach (preg_split('/\W+/', $title) ?: [] as $word) {
            if (strlen($word) >= 4 && str_contains($messageLower, $word)) $score += 3;
        }
        if ($score > $bestScore) { $best = $item; $bestScore = $score; }
    }
    return $bestScore > 0 ? $best : null;
}

function ai_complete_item_response(array $actor, string $hubId, array $post): void
{
    if (!can_complete_items($actor)) json_response(['ok'=>false, 'message'=>'Your current role cannot complete family items.'], 403);
    $message = trim((string)($post['message'] ?? ''));
    $item = find_pending_item_for_completion($hubId, $message);
    if (!$item) json_response(['ok'=>false, 'message'=>'I could not match that message to a pending task or chore. Open Todo List and press Done on the exact item.'], 404);
    if (item_requires_proof($item)) json_response(['ok'=>false, 'message'=>'Homework cannot be completed by Joshleen AI. Please upload the homework in Todo List and wait for parent approval.'], 422);
    $result = complete_item_record((string)$actor['id'], $hubId, (string)$item['id'], 'Joshleen AI chat completion');
    json_response(['ok'=>!empty($result['ok']), 'message'=>$result['message'] ?? 'Item completion processed.', 'item'=>['id'=>$item['id'], 'title'=>$item['title'], 'type'=>$item['type']]]);
}

function ai_chat_upload_response(array $actor, string $hubId, array $post): void
{
    if (!can_edit_items($actor)) json_response(['ok'=>false, 'message'=>'Your current role cannot upload files in Joshleen AI.'], 403);
    $message = trim((string)($post['message'] ?? ''));
    $records = save_chat_uploads($hubId, (string)$actor['id'], $message, 'chat_files');
    json_response([
        'ok'=>true,
        'message'=>count($records) ? 'File uploaded in Joshleen AI chat. For homework review, open the exact Todo item and use Upload homework for parent review.' : 'No file was uploaded.',
        'uploads'=>array_map(fn($r)=>['name'=>$r['original_name'], 'path'=>$r['path']], $records)
    ]);
}

function normalize_item_payload(array $data): array
{
    $type = in_array($data['type'] ?? '', ['task','chore','event','meal'], true) ? $data['type'] : 'task';
    $title = trim((string)($data['title'] ?? ''));
    if ($title === '') return ['error' => 'Title is required.'];
    $points = max(0, min(100, (int)($data['points'] ?? 10)));
    if ($type === 'event') $points = 0;
    $stars = max(0, min(10, (int)($data['stars'] ?? ($type === 'chore' ? 1 : 0))));
    $repeat = in_array($data['repeat'] ?? '', ['none','daily','weekly','monthly'], true) ? $data['repeat'] : 'none';
    $reminder = max(0, min(1440, (int)($data['reminder_minutes'] ?? 30)));
    $syncProvider = trim((string)($data['sync_provider'] ?? 'HomeSync')) ?: 'HomeSync';
    $syncMode = trim((string)($data['sync_mode'] ?? 'local')) ?: 'local';
    $member = trim((string)($data['member'] ?? 'Family')) ?: 'Family';
    $category = trim((string)($data['category'] ?? ucfirst($type))) ?: ucfirst($type);
    $notes = trim((string)($data['notes'] ?? ''));
    $proofRequired = isset($data['proof_required']) ? !empty($data['proof_required']) : inferred_proof_required($type, $title, $category, $notes);
    return [
        'id'=>uid('item_'), 'type'=>$type, 'title'=>$title,
        'member'=>$member,
        'category'=>$category,
        'meal_slot'=>trim((string)($data['meal_slot'] ?? '')),
        'start'=>trim((string)($data['start'] ?? '')), 'end'=>trim((string)($data['end'] ?? '')),
        'location'=>trim((string)($data['location'] ?? '')), 'notes'=>$notes,
        'invitees'=>trim((string)($data['invitees'] ?? '')), 'repeat'=>$repeat, 'points'=>$points, 'stars'=>$stars,
        'reminder_minutes'=>$reminder, 'sync_provider'=>$syncProvider, 'sync_mode'=>$syncMode,
        'proof_required'=>$proofRequired, 'proof_status'=>$proofRequired ? 'required' : 'not_required', 'proof_upload_id'=>'', 'verification_message'=>'', 'device_unlock_id'=>'',
        'status'=>'pending', 'created_at'=>app_now()->format(DateTimeInterface::ATOM), 'completed_at'=>'',
    ];
}

function create_item_record(string $userId, array $data): array
{
    $item = normalize_item_payload($data);
    if (isset($item['error'])) return $item;
    $store = read_store();
    $item['user_id'] = $userId;
    $store['items'][] = $item;
    write_store($store);
    return $item;
}

function create_item(string $userId, array $data): void
{
    $item = create_item_record($userId, $data);
    if (isset($item['error'])) { flash('error', $item['error']); return; }
    flash('success', ucfirst((string)($item['type'] ?? 'task')) . ' saved.');
}

function json_response(array $payload, int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($payload, JSON_UNESCAPED_SLASHES);
    exit;
}

function ai_create_item_response(array $actor, string $hubId, array $post): void
{
    if (!can_edit_items($actor)) json_response(['ok'=>false, 'message'=>'Your current role cannot create family items.'], 403);
    $item = create_item_record($hubId, $post);
    if (isset($item['error'])) json_response(['ok'=>false, 'message'=>$item['error']], 422);
    json_response([
        'ok'=>true,
        'message'=>ucfirst((string)$item['type']) . ' created: ' . (string)$item['title'],
        'item'=>[
            'id'=>$item['id'], 'type'=>$item['type'], 'title'=>$item['title'], 'start'=>$item['start'],
            'reminder_minutes'=>$item['reminder_minutes'], 'points'=>$item['points'], 'stars'=>$item['stars']
        ]
    ]);
}

function ai_update_item_reminder_response(array $actor, string $hubId, array $post): void
{
    if (!can_edit_items($actor)) json_response(['ok'=>false, 'message'=>'Your current role cannot update reminders.'], 403);
    $itemId = (string)($post['item_id'] ?? '');
    $minutes = max(0, min(1440, (int)($post['reminder_minutes'] ?? 30)));
    if ($itemId === '') json_response(['ok'=>false, 'message'=>'No item was selected for reminder update.'], 422);
    $store = read_store();
    foreach ($store['items'] as $idx => $item) {
        if (($item['id'] ?? '') === $itemId && ($item['user_id'] ?? '') === $hubId) {
            $store['items'][$idx]['reminder_minutes'] = $minutes;
            write_store($store);
            json_response(['ok'=>true, 'message'=>'Reminder updated to ' . $minutes . ' minutes before: ' . (string)($item['title'] ?? 'item')]);
        }
    }
    json_response(['ok'=>false, 'message'=>'I could not find that item to update.'], 404);
}

function complete_item(string $actorUserId, string $hubId, string $itemId): void
{
    $item = find_item_by_id($hubId, $itemId);
    if ($item && item_requires_proof($item)) {
        flash('info', 'This homework task requires a photo/file upload and parent approval before completion.');
        return;
    }
    $result = complete_item_record($actorUserId, $hubId, $itemId, 'Direct completion');
    flash(!empty($result['ok']) ? 'success' : 'error', (string)($result['message'] ?? 'Unable to complete item.'));
}

function complete_item_with_proof(array $actor, string $hubId, string $itemId): void
{
    $result = submit_homework_proof_for_parent_review($actor, $hubId, $itemId, 'proof_file');
    flash(!empty($result['ok']) ? 'success' : 'error', (string)($result['message'] ?? 'Homework proof could not be uploaded.'));
}

function approve_homework_proof(array $actor, string $hubId, string $itemId): void
{
    if (!can_manage_rewards($actor)) { permission_denied('Only a parent, Family Hub Admin, or Super Admin can approve homework.'); return; }
    $store = read_store();
    foreach ($store['items'] as $idx => $item) {
        if (($item['id'] ?? '') === $itemId && ($item['user_id'] ?? '') === $hubId) {
            $proofId = (string)($item['proof_upload_id'] ?? '');
            if ($proofId === '') { flash('error', 'No homework upload found. Ask the child to upload the homework first.'); return; }
            $store['items'][$idx]['proof_status'] = 'parent_approved';
            $store['items'][$idx]['verification_message'] = 'Parent approved this homework.';
            foreach ($store['proof_uploads'] as $pidx => $proof) {
                if (($proof['id'] ?? '') === $proofId) {
                    $store['proof_uploads'][$pidx]['status'] = 'parent_approved';
                    $store['proof_uploads'][$pidx]['verification_message'] = 'Parent approved this homework.';
                    $store['proof_uploads'][$pidx]['verified_at'] = app_now()->format(DateTimeInterface::ATOM);
                }
            }
            write_store($store);
            $result = complete_item_record((string)$actor['id'], $hubId, $itemId, 'Parent-approved homework proof');
            flash(!empty($result['ok']) ? 'success' : 'error', (string)($result['message'] ?? 'Unable to approve homework.'));
            return;
        }
    }
    flash('error', 'Homework item not found.');
}

function request_homework_correction(array $actor, string $hubId, string $itemId, string $comment = ''): void
{
    if (!can_manage_rewards($actor)) { permission_denied('Only a parent, Family Hub Admin, or Super Admin can request homework correction.'); return; }
    $comment = trim($comment);
    $message = $comment !== '' ? ('Parent requested correction: ' . $comment) : 'Parent requested correction. Please redo the homework and upload a new photo.';
    $store = read_store();
    foreach ($store['items'] as $idx => $item) {
        if (($item['id'] ?? '') === $itemId && ($item['user_id'] ?? '') === $hubId) {
            $proofId = (string)($item['proof_upload_id'] ?? '');
            if ($proofId === '') { flash('error', 'No homework upload found. Ask the child to upload the homework first.'); return; }
            $store['items'][$idx]['proof_status'] = 'needs_correction';
            $store['items'][$idx]['verification_message'] = $message;
            foreach ($store['proof_uploads'] as $pidx => $proof) {
                if (($proof['id'] ?? '') === $proofId) {
                    $store['proof_uploads'][$pidx]['status'] = 'needs_correction';
                    $store['proof_uploads'][$pidx]['verification_message'] = $message;
                    $store['proof_uploads'][$pidx]['verified_at'] = app_now()->format(DateTimeInterface::ATOM);
                }
            }
            write_store($store);
            flash('success', 'Homework sent back for correction.');
            return;
        }
    }
    flash('error', 'Homework item not found.');
}

function delete_item(string $userId, string $itemId): void { $store=read_store(); $before=count($store['items']); $store['items']=array_values(array_filter($store['items'], fn($x)=>!(($x['id'] ?? '')===$itemId && ($x['user_id'] ?? '')===$userId))); write_store($store); flash(count($store['items'])<$before?'success':'error', count($store['items'])<$before?'Item deleted.':'Item not found.'); }

function create_list_item(string $userId, array $data): void
{
    $store=read_store(); $type=in_array($data['list_type'] ?? '', ['grocery','todo','reminder','errand'], true) ? $data['list_type'] : 'todo'; $title=trim((string)($data['title'] ?? ''));
    if ($title==='') { flash('error','List item title is required.'); return; }
    $store['list_items'][]=['id'=>uid('list_'),'user_id'=>$userId,'list_type'=>$type,'title'=>$title,'notes'=>trim((string)($data['notes'] ?? '')),'status'=>'open','created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    write_store($store); flash('success','List item added.');
}
function toggle_list_item(string $userId,string $id): void { $s=read_store(); foreach($s['list_items'] as $i=>$x) if(($x['id']??'')===$id && ($x['user_id']??'')===$userId){ $s['list_items'][$i]['status']=(($x['status']??'open')==='done')?'open':'done'; write_store($s); flash('success','List updated.'); return; } flash('error','List item not found.'); }
function delete_list_item(string $userId,string $id): void { $s=read_store(); $b=count($s['list_items']); $s['list_items']=array_values(array_filter($s['list_items'], fn($x)=>!(($x['id']??'')===$id && ($x['user_id']??'')===$userId))); write_store($s); flash(count($s['list_items'])<$b?'success':'error',count($s['list_items'])<$b?'List item deleted.':'List item not found.'); }

function save_connection(string $userId, array $data): void
{
    $provider = trim((string)($data['provider'] ?? 'Google Calendar'));
    $mode = in_array($data['sync_mode'] ?? '', ['one-way','two-way-ready','local-feed'], true) ? $data['sync_mode'] : 'one-way';
    $label = trim((string)($data['label'] ?? 'Family calendar')) ?: 'Family calendar';
    $account = trim((string)($data['account'] ?? ''));
    $s=read_store(); $s['connections'][]=['id'=>uid('conn_'),'user_id'=>$userId,'provider'=>$provider,'label'=>$label,'account'=>$account,'sync_mode'=>$mode,'status'=>'Ready to configure','created_at'=>app_now()->format(DateTimeInterface::ATOM),'last_sync'=>'Not synced yet']; write_store($s); flash('success','Calendar connection saved as an integration-ready record.');
}
function delete_connection(string $userId,string $id): void { $s=read_store(); $s['connections']=array_values(array_filter($s['connections'], fn($x)=>!(($x['id']??'')===$id && ($x['user_id']??'')===$userId))); write_store($s); flash('success','Connection removed.'); }

function create_share_invite(string $hubId, array $data, ?array $actor=null): void
{
    $s=read_store(); $name=trim((string)($data['share_name'] ?? 'Family member')) ?: 'Family member';
    $allowed=editable_role_choices_for($actor); $requested=(string)($data['share_role'] ?? 'Viewer'); $role=in_array($requested,$allowed,true) ? $requested : 'Viewer';
    $s['share_invites'][]=['id'=>uid('share_'),'user_id'=>$hubId,'created_by'=>$actor['id'] ?? $hubId,'name'=>$name,'role'=>$role,'code'=>strtoupper(bin2hex(random_bytes(3))),'created_at'=>app_now()->format(DateTimeInterface::ATOM),'status'=>'active']; write_store($s); flash('success','Share access invite created for ' . $role . '.');
}
function revoke_share(string $userId,string $id): void { $s=read_store(); foreach($s['share_invites'] as $i=>$x) if(($x['id']??'')===$id && ($x['user_id']??'')===$userId){$s['share_invites'][$i]['status']='revoked';} write_store($s); flash('success','Access revoked.'); }

function add_photo_card(string $userId, array $data): void
{
    $title=trim((string)($data['photo_title'] ?? 'Family memory')) ?: 'Family memory'; $caption=trim((string)($data['photo_caption'] ?? ''));
    $color=trim((string)($data['photo_color'] ?? '#49d9ff')); if(!preg_match('/^#[0-9a-fA-F]{6}$/',$color)) $color='#49d9ff';
    $s=read_store(); $s['photos'][]=['id'=>uid('photo_'),'user_id'=>$userId,'title'=>$title,'caption'=>$caption,'color'=>$color,'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)]; write_store($s); flash('success','Screensaver card added.');
}

function magic_import(string $userId, string $text): void
{
    $created = 0; $store = read_store();
    $lines = preg_split('/\R+/', trim($text));
    foreach ($lines ?: [] as $line) {
        $line = trim($line); if ($line === '') continue;
        if (preg_match('/^(Grocery|Todo|Task|Chore|Meal|Event)\s*:\s*(.+)$/i', $line, $m)) {
            $kind=strtolower($m[1]); $title=trim($m[2]);
            if ($kind === 'grocery' || $kind === 'todo') { $store['list_items'][]=['id'=>uid('list_'),'user_id'=>$userId,'list_type'=>$kind==='grocery'?'grocery':'todo','title'=>$title,'notes'=>'Imported by Magic Import','status'=>'open','created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)]; $created++; continue; }
            $type = $kind === 'event' ? 'event' : ($kind === 'meal' ? 'meal' : ($kind === 'chore' ? 'chore' : 'task'));
            $store['items'][]=['id'=>uid('item_'),'user_id'=>$userId,'type'=>$type,'title'=>$title,'member'=>'Family','category'=>ucfirst($type),'meal_slot'=>$type==='meal'?'Dinner':'','start'=>'','end'=>'','location'=>'','notes'=>'Imported by Magic Import','invitees'=>'','repeat'=>'none','points'=>$type==='event'?0:10,'stars'=>$type==='chore'?1:0,'reminder_minutes'=>15,'sync_provider'=>'HomeSync','sync_mode'=>'local','status'=>'pending','created_at'=>app_now()->format(DateTimeInterface::ATOM),'completed_at'=>'']; $created++; continue;
        }
        if (preg_match('/^(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}))?\s+(.+)$/', $line, $m)) {
            $start = $m[1] . 'T' . (($m[2] ?? '') ?: '09:00');
            $store['items'][]=['id'=>uid('item_'),'user_id'=>$userId,'type'=>'event','title'=>trim($m[3]),'member'=>'Family','category'=>'Imported','meal_slot'=>'','start'=>$start,'end'=>'','location'=>'','notes'=>'Detected date from Magic Import','invitees'=>'','repeat'=>'none','points'=>0,'stars'=>0,'reminder_minutes'=>15,'sync_provider'=>'HomeSync','sync_mode'=>'local','status'=>'pending','created_at'=>app_now()->format(DateTimeInterface::ATOM),'completed_at'=>'']; $created++;
        }
    }
    write_store($store); flash($created ? 'success' : 'info', $created ? "Magic Import created {$created} item(s)." : 'No importable lines detected. Use prefixes like Event:, Task:, Chore:, Meal:, Grocery:, or YYYY-MM-DD 18:30 Event title.');
}

function sidekick_generate(string $userId, array $data): void
{
    $days=max(1,min(7,(int)($data['days'] ?? 3))); $focus=trim((string)($data['focus'] ?? 'balanced family meals')) ?: 'balanced family meals';
    $store=read_store(); $base=app_now(); $created=0; $meals=['Teriyaki chicken rice','Tuna pasta bake','Veggie omelette','Beef stir fry','Salmon bowl','Chicken wraps','Tomato soup and toast'];
    for($i=0;$i<$days;$i++){ $day=$base->modify('+' . $i . ' days'); $title=$meals[$i % count($meals)]; $store['items'][]=['id'=>uid('item_'),'user_id'=>$userId,'type'=>'meal','title'=>$title,'member'=>'Family','category'=>'Dinner','meal_slot'=>'Dinner','start'=>$day->setTime(19,0)->format('Y-m-d\TH:i'),'end'=>'','location'=>'Kitchen','notes'=>"Generated by Sidekick for: {$focus}\nprotein\nvegetable\ncarbs",'invitees'=>'','repeat'=>'none','points'=>10,'stars'=>1,'reminder_minutes'=>15,'sync_provider'=>'HomeSync','sync_mode'=>'local','status'=>'pending','created_at'=>app_now()->format(DateTimeInterface::ATOM),'completed_at'=>'']; $created++; }
    $store['activity'][]=['id'=>uid('act_'),'user_id'=>$userId,'item_id'=>'','title'=>'Sidekick generated meal plan','type'=>'sidekick','member'=>'Family','points'=>0,'stars'=>0,'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)]; write_store($store); flash('success',"Sidekick created {$created} dinner plan(s).");
}

function parse_dt(?string $value): ?DateTimeImmutable { if(!$value) return null; try { return new DateTimeImmutable($value, new DateTimeZone(APP_TZ)); } catch(Throwable $e) { return null; } }
function format_dt(?string $value,string $fallback='Anytime'): string { $dt=parse_dt($value); return $dt ? $dt->format('D, d M • h:i A') : $fallback; }
function format_time(?string $value,string $fallback='Anytime'): string { $dt=parse_dt($value); return $dt ? $dt->format('h:i A') : $fallback; }
function item_date(?string $value): string { $dt=parse_dt($value); return $dt ? $dt->format('Y-m-d') : 'Unscheduled'; }
function date_label(string $date): string { if($date==='Unscheduled') return 'Unscheduled'; $today=app_now()->format('Y-m-d'); $tom=app_now()->modify('+1 day')->format('Y-m-d'); if($date===$today) return 'Today'; if($date===$tom) return 'Tomorrow'; try{return (new DateTimeImmutable($date))->format('D, d M');}catch(Throwable $e){return $date;} }
function week_dates(?string $selected=null): array { $base=$selected?new DateTimeImmutable($selected,new DateTimeZone(APP_TZ)):app_now(); $monday=$base->modify('monday this week'); $days=[]; for($i=0;$i<7;$i++)$days[]=$monday->modify('+' . $i . ' days'); return $days; }
function month_dates(string $selected): array { $base=new DateTimeImmutable($selected,new DateTimeZone(APP_TZ)); $start=$base->modify('first day of this month')->modify('monday this week'); $days=[]; for($i=0;$i<42;$i++)$days[]=$start->modify('+' . $i . ' days'); return $days; }
function upcoming_items(array $items,int $limit=8): array { $now=app_now()->format('Y-m-d\TH:i'); $p=array_values(array_filter($items, fn($i)=>(($i['status']??'pending')!=='done') && (($i['start']??'')==='' || ($i['start']??'') >= $now))); usort($p,fn($a,$b)=>strcmp($a['start']??'9999',$b['start']??'9999')); return array_slice($p,0,$limit); }
function items_on_date(array $items,string $date): array { return array_values(array_filter($items, fn($i)=>item_date($i['start'] ?? '')===$date)); }
function grouped_by_date(array $items): array { $g=[]; foreach($items as $i){$g[item_date($i['start']??'')][]=$i;} uksort($g,fn($a,$b)=>$a==='Unscheduled'?1:($b==='Unscheduled'?-1:strcmp($a,$b))); return $g; }
function stats_for(string $userId): array { $items=items_for($userId); $today=app_now()->format('Y-m-d'); $todayItems=array_values(array_filter($items, fn($i)=>item_date($i['start'] ?? '')===$today)); $doneToday=array_values(array_filter($todayItems, fn($i)=>($i['status']??'')==='done')); $pending=array_values(array_filter($items, fn($i)=>($i['status']??'pending')!=='done')); return ['all'=>count($items),'today'=>count($todayItems),'done_today'=>count($doneToday),'pending'=>count($pending),'chores_pending'=>count(array_filter($items,fn($i)=>($i['type']??'')==='chore'&&($i['status']??'pending')!=='done')),'meals_today'=>count(array_filter($todayItems,fn($i)=>($i['type']??'')==='meal')),'completion'=>count($todayItems)?round((count($doneToday)/count($todayItems))*100):0]; }
function type_icon(string $type): string { return match($type){'event'=>'📅','chore'=>'🧹','meal'=>'🍽️','sidekick'=>'✨',default=>'✅'}; }
function type_label(string $type): string { return ucfirst($type ?: 'task'); }
function grocery_suggestions(array $meals,array $existing): array { $have=[]; foreach($existing as $x) if(($x['list_type']??'')==='grocery') $have[strtolower(trim($x['title']??''))]=true; $s=[]; foreach($meals as $m){foreach(preg_split('/[\n,;]+/', (string)($m['notes']??'')) ?: [] as $p){$name=trim($p); $key=strtolower($name); if($name!=='' && !isset($have[$key]) && !isset($s[$key]) && !str_starts_with($key,'generated by')) $s[$key]=$name;}} return array_values($s); }
function is_sleeping(array $settings): bool { if(empty($settings['sleep_enabled'])) return false; $now=app_now()->format('H:i'); $start=$settings['sleep_start'] ?? '22:30'; $end=$settings['sleep_end'] ?? '06:30'; return $start < $end ? ($now >= $start && $now <= $end) : ($now >= $start || $now <= $end); }



function find_active_workspace_by_family_hub_code(array $store, string $code): ?array
{
    $code = normalize_join_code($code);
    if ($code === '') return null;
    foreach (($store['workspaces'] ?? []) as $ws) {
        if (strtoupper((string)($ws['join_code'] ?? '')) === $code && ($ws['status'] ?? 'active') === 'active') return $ws;
    }
    return null;
}

function link_user_to_family_hub_in_store(array &$store, string $userId, string $workspaceId, string $role='Member'): void
{
    $found = false;
    foreach (($store['workspace_members'] ?? []) as $idx => $m) {
        if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === $userId) {
            $store['workspace_members'][$idx]['status'] = 'active';
            $store['workspace_members'][$idx]['role'] = normalize_workspace_role((string)($m['role'] ?? $role));
            $store['workspace_members'][$idx]['restored_at'] = app_now()->format(DateTimeInterface::ATOM);
            $found = true;
            break;
        }
    }
    if (!$found) {
        $store['workspace_members'][] = [
            'id'=>uid('member_'),
            'workspace_id'=>$workspaceId,
            'user_id'=>$userId,
            'role'=>normalize_workspace_role($role),
            'status'=>'active',
            'created_at'=>app_now()->format(DateTimeInterface::ATOM),
        ];
    }
    foreach ($store['users'] as $idx => $u) {
        if (($u['id'] ?? '') !== $userId) continue;
        if (!in_array($workspaceId, $store['users'][$idx]['workspace_ids'] ?? [], true)) $store['users'][$idx]['workspace_ids'][] = $workspaceId;
        $store['users'][$idx]['active_workspace_id'] = $workspaceId;
    }
    $store['workspace_history'][] = ['id'=>uid('hist_'),'type'=>'join_family_hub','workspace_id'=>$workspaceId,'user_id'=>$userId,'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
}

function create_personal_family_hub(array $user, string $name): void
{
    $name = trim($name) ?: ((string)($user['name'] ?? 'Family') . "'s Family Hub");
    $store = read_store();
    $workspaceId = create_private_workspace_for_user($store, (string)$user['id'], $name, 'Member');
    write_store($store);
    $_SESSION['active_workspace_id'] = $workspaceId;
    seed_demo_items($workspaceId);
    flash('success', 'New Family Hub created. Your account is linked as Member.');
}


function set_active_workspace_for_current_user(string $workspaceId): void
{
    $user = require_user();
    if (!user_can_access_workspace($user, $workspaceId)) { flash('error', 'Workspace is unavailable or you do not have access.'); return; }
    $_SESSION['active_workspace_id'] = $workspaceId;
    $store = read_store();
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === ($user['id'] ?? '')) $store['users'][$idx]['active_workspace_id'] = $workspaceId;
    write_store($store);
    flash('success', 'Active Family Hub switched to ' . workspace_name($workspaceId) . '.');
}
function create_private_workspace_for_user(array &$store, string $userId, string $name, string $role='Member'): string
{
    $workspaceId = uid('workspace_');
    $store['workspaces'][] = [
        'id' => $workspaceId,
        'name' => trim($name) ?: 'Private Workspace',
        'join_code' => generate_join_code($store),
        'owner_user_id' => $userId,
        'created_by' => $userId,
        'status' => 'active',
        'created_at' => app_now()->format(DateTimeInterface::ATOM),
    ];
    $store['workspace_members'][] = [
        'id' => uid('member_'),
        'workspace_id' => $workspaceId,
        'user_id' => $userId,
        'role' => normalize_workspace_role($role),
        'status' => 'active',
        'created_at' => app_now()->format(DateTimeInterface::ATOM),
    ];
    $store['settings'][$workspaceId] = default_settings();
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === $userId) {
        $store['users'][$idx]['active_workspace_id'] = $workspaceId;
        $store['users'][$idx]['workspace_ids'][] = $workspaceId;
    }
    return $workspaceId;
}

function join_workspace_by_id(array $user, string $workspaceId): void
{
    $workspaceId = trim($workspaceId);
    if ($workspaceId === '') { flash('error','Choose a Family Hub to join.'); return; }
    $store = read_store();
    $target = find_active_workspace_by_id($store, $workspaceId);
    if (!$target || $workspaceId === 'workspace_system') { flash('error','Family Hub is unavailable or archived.'); return; }
    foreach (($store['workspace_members'] ?? []) as $idx => $m) {
        if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === ($user['id'] ?? '')) {
            if (($m['status'] ?? 'active') === 'active') {
                $_SESSION['active_workspace_id'] = $workspaceId;
                flash('info','You are already linked to this Family Hub.');
                return;
            }
            $store['workspace_members'][$idx]['status'] = 'active';
            $store['workspace_members'][$idx]['role'] = normalize_workspace_role((string)($m['role'] ?? 'Member'));
            $store['workspace_members'][$idx]['restored_at'] = app_now()->format(DateTimeInterface::ATOM);
            foreach ($store['users'] as $uIdx => $u) {
                if (($u['id'] ?? '') === ($user['id'] ?? '')) {
                    if (!in_array($workspaceId, $store['users'][$uIdx]['workspace_ids'] ?? [], true)) $store['users'][$uIdx]['workspace_ids'][] = $workspaceId;
                    $store['users'][$uIdx]['active_workspace_id'] = $workspaceId;
                }
            }
            write_store($store);
            $_SESSION['active_workspace_id'] = $workspaceId;
            flash('success','Family Hub restored and linked to your account.');
            return;
        }
    }
    $store['workspace_members'][] = [
        'id'=>uid('member_'),
        'workspace_id'=>$workspaceId,
        'user_id'=>(string)$user['id'],
        'role'=>'Member',
        'status'=>'active',
        'created_at'=>app_now()->format(DateTimeInterface::ATOM),
    ];
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === ($user['id'] ?? '')) {
        if (!in_array($workspaceId, $store['users'][$idx]['workspace_ids'] ?? [], true)) $store['users'][$idx]['workspace_ids'][] = $workspaceId;
        $store['users'][$idx]['active_workspace_id'] = $workspaceId;
    }
    $store['workspace_history'][] = ['id'=>uid('hist_'),'type'=>'join_workspace','workspace_id'=>$workspaceId,'user_id'=>$user['id'],'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    write_store($store);
    $_SESSION['active_workspace_id'] = $workspaceId;
    flash('success','You joined ' . ($target['name'] ?? 'the Family Hub') . '. Your original Family Hub remains linked.');
}

function join_workspace_by_code(array $user, string $code): void
{
    $code = normalize_join_code($code);
    if ($code === '') { flash('error','Choose a Family Hub to join.'); return; }
    $store = read_store(); $target = null;
    foreach (($store['workspaces'] ?? []) as $ws) {
        if (strtoupper((string)($ws['join_code'] ?? '')) === $code && ($ws['status'] ?? 'active') === 'active') { $target = $ws; break; }
    }
    if (!$target) { flash('error','Family Hub is unavailable or archived.'); return; }
    $workspaceId = (string)$target['id'];
    foreach (($store['workspace_members'] ?? []) as $idx => $m) {
        if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === ($user['id'] ?? '')) {
            if (($m['status'] ?? 'active') === 'active') { flash('info','You are already linked to this Family Hub.'); $_SESSION['active_workspace_id']=$workspaceId; return; }
            $store['workspace_members'][$idx]['status'] = 'active';
            $store['workspace_members'][$idx]['role'] = normalize_workspace_role((string)($m['role'] ?? 'Member'));
            $store['workspace_members'][$idx]['restored_at'] = app_now()->format(DateTimeInterface::ATOM);
            write_store($store); $_SESSION['active_workspace_id']=$workspaceId; flash('success','Family Hub restored and linked to your account.'); return;
        }
    }
    $store['workspace_members'][] = [
        'id'=>uid('member_'), 'workspace_id'=>$workspaceId, 'user_id'=>(string)$user['id'],
        'role'=>'Member', 'status'=>'active', 'created_at'=>app_now()->format(DateTimeInterface::ATOM),
    ];
    foreach ($store['users'] as $idx => $u) if (($u['id'] ?? '') === ($user['id'] ?? '')) {
        if (!in_array($workspaceId, $store['users'][$idx]['workspace_ids'] ?? [], true)) $store['users'][$idx]['workspace_ids'][] = $workspaceId;
        $store['users'][$idx]['active_workspace_id'] = $workspaceId;
    }
    $store['workspace_history'][] = ['id'=>uid('hist_'),'type'=>'join_workspace','workspace_id'=>$workspaceId,'user_id'=>$user['id'],'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    write_store($store); $_SESSION['active_workspace_id']=$workspaceId; flash('success','You joined ' . ($target['name'] ?? 'the Family Hub') . '. Your original Family Hub remains linked.');
}
function leave_workspace(array $user, string $workspaceId): void
{
    if (is_super_admin($user)) { flash('error','Super Admin should switch or archive Family Hubs instead of leaving.'); return; }
    if (active_workspace_count_for_user((string)$user['id']) <= 1) { flash('error','You cannot leave your last active family hub. Join or create another family hub first.'); return; }
    $store = read_store(); $changed=false;
    foreach (($store['workspace_members'] ?? []) as $idx => $m) {
        if (($m['workspace_id'] ?? '') === $workspaceId && ($m['user_id'] ?? '') === ($user['id'] ?? '') && ($m['status'] ?? 'active') === 'active') {
            $store['workspace_members'][$idx]['status']='left';
            $store['workspace_members'][$idx]['left_at']=app_now()->format(DateTimeInterface::ATOM);
            $changed=true; break;
        }
    }
    if (!$changed) { flash('error','Family Hub membership not found.'); return; }
    $next = '';
    foreach (accessible_workspaces_for_user($user, $store, false) as $ws) if (($ws['id'] ?? '') !== $workspaceId) { $next = (string)$ws['id']; break; }
    foreach ($store['users'] as $idx=>$u) if (($u['id'] ?? '') === ($user['id'] ?? '')) $store['users'][$idx]['active_workspace_id']=$next;
    $store['workspace_history'][]=['id'=>uid('hist_'),'type'=>'leave_workspace','workspace_id'=>$workspaceId,'user_id'=>$user['id'],'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    write_store($store); $_SESSION['active_workspace_id']=$next; flash('success','You left the Family Hub. Your other Family Hubs remain available.');
}
function create_workspace_as_super_admin(array $actor, array $data): void
{
    if (!is_super_admin($actor)) { permission_denied('Only Super Admin can create workspaces manually.'); return; }
    $name = trim((string)($data['workspace_name'] ?? '')) ?: 'New Family Workspace';
    $store = read_store();
    $workspaceId = uid('workspace_');
    $store['workspaces'][] = ['id'=>$workspaceId,'name'=>$name,'join_code'=>generate_join_code($store),'created_by'=>$actor['id'],'owner_user_id'=>'','status'=>'active','created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    $store['settings'][$workspaceId] = default_settings();
    $store['workspace_history'][]=['id'=>uid('hist_'),'type'=>'create_workspace','workspace_id'=>$workspaceId,'user_id'=>$actor['id'],'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
    write_store($store); flash('success','Family Hub created.');
}
function archive_or_restore_workspace(array $actor, string $workspaceId, string $status): void
{
    if (!is_super_admin($actor)) { permission_denied('Only Super Admin can archive or restore workspaces.'); return; }
    $store=read_store(); $done=false;
    foreach ($store['workspaces'] as $idx=>$ws) if (($ws['id'] ?? '') === $workspaceId) {
        $store['workspaces'][$idx]['status']=$status;
        $store['workspaces'][$idx][($status==='archived'?'archived_at':'restored_at')] = app_now()->format(DateTimeInterface::ATOM);
        $store['workspace_history'][]=['id'=>uid('hist_'),'type'=>$status==='archived'?'archive_workspace':'restore_workspace','workspace_id'=>$workspaceId,'user_id'=>$actor['id'],'created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
        $done=true; break;
    }
    write_store($store); flash($done?'success':'error',$done ? 'Workspace status updated.' : 'Workspace not found.');
}

function delete_workspace_permanently(array $actor, string $workspaceId): void
{
    if (!is_super_admin($actor)) { permission_denied('Only Super Admin can permanently delete workspaces.'); return; }
    $workspaceId = trim($workspaceId);
    if ($workspaceId === '') { flash('error', 'Workspace not found.'); return; }
    if ($workspaceId === 'workspace_system') { flash('error', 'The protected Super Admin workspace cannot be deleted.'); return; }
    $store = read_store();
    $target = workspace_by_id($store, $workspaceId);
    if (!$target) { flash('error', 'Workspace not found.'); return; }
    $before = count($store['workspaces'] ?? []);

    // Hard-delete the workspace and all workspace-scoped data so it is no longer accessible.
    $store['workspaces'] = array_values(array_filter($store['workspaces'] ?? [], fn($ws) => ($ws['id'] ?? '') !== $workspaceId));
    $store['workspace_members'] = array_values(array_filter($store['workspace_members'] ?? [], fn($m) => ($m['workspace_id'] ?? '') !== $workspaceId));
    $store['items'] = array_values(array_filter($store['items'] ?? [], fn($item) => ($item['user_id'] ?? '') !== $workspaceId));
    $store['list_items'] = array_values(array_filter($store['list_items'] ?? [], fn($item) => ($item['user_id'] ?? '') !== $workspaceId));
    $store['connections'] = array_values(array_filter($store['connections'] ?? [], fn($c) => ($c['user_id'] ?? '') !== $workspaceId));
    $store['share_invites'] = array_values(array_filter($store['share_invites'] ?? [], fn($inv) => ($inv['workspace_id'] ?? $inv['family_id'] ?? '') !== $workspaceId));
    $store['photos'] = array_values(array_filter($store['photos'] ?? [], fn($photo) => ($photo['user_id'] ?? '') !== $workspaceId));
    unset($store['settings'][$workspaceId]);

    foreach ($store['users'] as $idx => $u) {
        $store['users'][$idx]['workspace_ids'] = array_values(array_filter($u['workspace_ids'] ?? [], fn($wid) => $wid !== $workspaceId));
        if (($u['active_workspace_id'] ?? '') === $workspaceId) {
            $replacement = '';
            foreach (($store['workspace_members'] ?? []) as $m) {
                if (($m['user_id'] ?? '') === ($u['id'] ?? '') && ($m['status'] ?? 'active') === 'active') { $replacement = (string)($m['workspace_id'] ?? ''); break; }
            }
            $store['users'][$idx]['active_workspace_id'] = $replacement;
        }
    }
    $store['workspace_history'][] = [
        'id'=>uid('hist_'),
        'type'=>'delete_workspace_permanent',
        'workspace_id'=>$workspaceId,
        'workspace_name'=>(string)($target['name'] ?? 'Workspace'),
        'user_id'=>(string)($actor['id'] ?? ''),
        'created_at'=>app_now()->format(DateTimeInterface::ATOM),
    ];
    write_store($store);
    if (($_SESSION['active_workspace_id'] ?? '') === $workspaceId) $_SESSION['active_workspace_id'] = '';
    flash(count($store['workspaces'] ?? []) < $before ? 'success' : 'error', count($store['workspaces'] ?? []) < $before ? 'Workspace permanently deleted and is no longer accessible.' : 'Workspace could not be deleted.');
}

function update_workspace_member_role(array $actor, array $data): void
{
    $workspaceId = (string)($data['workspace_id'] ?? current_workspace_id_for_user($actor));
    if (!is_super_admin($actor) && !(workspace_role_for_user($actor, $workspaceId)==='Admin')) { permission_denied('Only workspace Admin or Super Admin can change member roles.'); return; }
    $targetMemberId = (string)($data['member_id'] ?? ''); $role=normalize_workspace_role((string)($data['member_role'] ?? 'Member'));
    $store=read_store(); $done=false;
    foreach ($store['workspace_members'] as $idx=>$m) if (($m['id'] ?? '') === $targetMemberId && ($m['workspace_id'] ?? '') === $workspaceId) {
        $store['workspace_members'][$idx]['role']=$role; $store['workspace_members'][$idx]['updated_at']=app_now()->format(DateTimeInterface::ATOM); $done=true; break;
    }
    write_store($store); flash($done?'success':'error',$done?'Member role updated.':'Member not found.');
}
function archive_or_restore_workspace_member(array $actor, array $data, string $status): void
{
    $workspaceId = (string)($data['workspace_id'] ?? current_workspace_id_for_user($actor));
    if (!is_super_admin($actor) && !(workspace_role_for_user($actor, $workspaceId)==='Admin')) { permission_denied('Only workspace Admin or Super Admin can archive or restore members.'); return; }
    $memberId=(string)($data['member_id'] ?? ''); $store=read_store(); $done=false;
    foreach ($store['workspace_members'] as $idx=>$m) if (($m['id'] ?? '') === $memberId && ($m['workspace_id'] ?? '') === $workspaceId) {
        $store['workspace_members'][$idx]['status']=$status;
        $store['workspace_members'][$idx][($status==='archived'?'archived_at':'restored_at')]=app_now()->format(DateTimeInterface::ATOM);
        $done=true; break;
    }
    write_store($store); flash($done?'success':'error',$done?'Member status updated.':'Member not found.');
}
function archive_or_restore_user(array $actor, string $targetUserId, string $status): void
{
    if (!is_super_admin($actor)) { permission_denied('Only Super Admin can archive or restore users.'); return; }
    $store=read_store(); $done=false;
    foreach ($store['users'] as $idx=>$u) if (($u['id'] ?? '') === $targetUserId && !is_super_admin_role((string)($u['role'] ?? ''))) {
        $store['users'][$idx]['status']=$status;
        $store['users'][$idx][($status==='archived'?'archived_at':'restored_at')] = app_now()->format(DateTimeInterface::ATOM);
        $done=true; break;
    }
    write_store($store); flash($done?'success':'error',$done?'User status updated.':'User not found or protected.');
}
function update_user_role(array $data): void
{
    $targetId = (string)($data['target_user_id'] ?? '');
    $role = (string)($data['target_role'] ?? 'Viewer');
    if (!in_array($role, role_options(), true)) { flash('error','Invalid role selected.'); return; }
    $s = read_store();
    foreach ($s['users'] as $idx=>$target) {
        if (($target['id'] ?? '') !== $targetId) continue;
        $s['users'][$idx]['role'] = $role;
        if (in_array($role, ['Superadmin','Owner'], true)) $s['users'][$idx]['family_id'] = $targetId;
        elseif (empty($s['users'][$idx]['family_id'])) $s['users'][$idx]['family_id'] = $targetId;
        write_store($s); flash('success','User role updated.'); return;
    }
    flash('error','User not found.');
}

function export_calendar_ics(string $userId): never
{
    $lines=['BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//HomeSyncHub//CommandCenter PWA//EN','CALSCALE:GREGORIAN','METHOD:PUBLISH'];
    foreach(items_for($userId,null,false) as $item){ if(empty($item['start'])) continue; $start=parse_dt($item['start']); if(!$start) continue; $end=parse_dt($item['end'] ?? '') ?: $start->modify('+30 minutes'); $lines[]='BEGIN:VEVENT'; $lines[]='UID:' . ($item['id'] ?? uid('ics_')) . '@homesynchub.local'; $lines[]='DTSTAMP:' . app_now()->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z'); $lines[]='DTSTART:' . $start->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z'); $lines[]='DTEND:' . $end->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z'); $lines[]='SUMMARY:' . ics_escape($item['title'] ?? 'HomeSyncHub item'); if(!empty($item['location'])) $lines[]='LOCATION:' . ics_escape($item['location']); $lines[]='DESCRIPTION:' . ics_escape(($item['type'] ?? 'task') . ' • ' . ($item['member'] ?? 'Family') . "\n" . ($item['notes'] ?? '')); $lines[]='CATEGORIES:' . ics_escape($item['category'] ?? ucfirst($item['type'] ?? 'task')); $lines[]='END:VEVENT'; }
    $lines[]='END:VCALENDAR'; header('Content-Type: text/calendar; charset=utf-8'); header('Content-Disposition: attachment; filename="homesynchub-calendar.ics"'); echo implode("\r\n",$lines) . "\r\n"; exit;
}
function ics_escape(string $v): string { return str_replace(['\\',';',',',"\n","\r"], ['\\\\','\\;','\\,','\\n',''], $v); }

function handle_get_actions(): void { if(($_GET['action'] ?? '') === 'export_ics'){ $u=require_user(); export_calendar_ics(hub_id_for_user($u)); } }

function handle_post(): void
{
    if(($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') return;
    $action=$_POST['action'] ?? ''; $store=read_store();
    if($action==='forgot_password'){ request_password_reset($_POST); redirect_to('login'); }
    if($action==='reset_password'){ reset_password_with_token($_POST); redirect_to('login'); }
    if($action==='verify_2fa'){
        $pendingId = (string)($_SESSION['pending_2fa_user_id'] ?? '');
        $pendingUser = $pendingId !== '' ? user_by_id($store, $pendingId) : null;
        if(!$pendingUser || ($pendingUser['status'] ?? 'active') !== 'active'){ unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_started_at']); flash('error','2FA session expired. Please log in again.'); redirect_to('login'); }
        if(!verify_totp_code((string)($pendingUser['two_factor_secret'] ?? ''), (string)($_POST['totp_code'] ?? ''))){ flash('error','Invalid authenticator code.'); redirect_to('login'); }
        $rememberAfter2fa = !empty($_SESSION['pending_remember_credentials']);
        unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_started_at'], $_SESSION['pending_remember_credentials']);
        $_SESSION['user_id']=$pendingUser['id']; $_SESSION['active_workspace_id']=current_workspace_id_for_user($pendingUser);
        if ($rememberAfter2fa) issue_remember_login_token($store, (string)$pendingUser['id']);
        else clear_current_remember_login($store);
        write_store($store);
        flash('success','Signed in successfully.'); redirect_to('dashboard');
    }
    if($action==='cancel_2fa'){ unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_started_at'], $_SESSION['pending_remember_credentials']); redirect_to('login'); }
    if($action==='register'){
        $name=trim((string)($_POST['name'] ?? '')); $email=trim((string)($_POST['email'] ?? '')); $password=(string)($_POST['password'] ?? '');
        $accountType = (($_POST['account_type'] ?? 'parent') === 'child') ? 'child' : 'parent';
        $familyHubMode = (($_POST['family_hub_mode'] ?? 'create') === 'join') ? 'join' : 'create';
        $familyHubName = trim((string)($_POST['family_hub_name'] ?? ''));
        $familyHubId = trim((string)($_POST['family_hub_id'] ?? $_POST['workspace_id'] ?? ''));
        $familyHubCode = normalize_join_code((string)($_POST['family_hub_code'] ?? $_POST['join_code'] ?? ''));
        if($accountType === 'child') $familyHubMode = 'join';
        if($name==='' || !filter_var($email,FILTER_VALIDATE_EMAIL) || strlen($password)<6){ flash('error','Enter a name, valid email, and password with at least 6 characters.'); redirect_to('login'); }
        if(empty($_POST['accept_terms'])){ flash('error','Please accept the HomeSyncHub Terms and Agreement to create an account.'); redirect_to('login'); }
        if(user_by_email($store,$email)){ flash('error','An account with that email already exists.'); redirect_to('login'); }
        $targetWorkspace = null;
        if($familyHubMode === 'join'){
            $targetWorkspace = $familyHubId !== '' ? find_active_workspace_by_id($store, $familyHubId) : null;
            if(!$targetWorkspace && $familyHubCode !== '') $targetWorkspace = find_active_workspace_by_family_hub_code($store, $familyHubCode);
            if(!$targetWorkspace || (string)($targetWorkspace['id'] ?? '') === 'workspace_system'){
                flash('error','Choose an available Family Hub to join.'); redirect_to('login');
            }
        }
        $userId = uid('user_');
        $workspaceRole = $accountType === 'parent' ? 'Admin' : 'Member';
        $user=['id'=>$userId,'name'=>$name,'email'=>$email,'role'=>'User','account_type'=>$accountType,'status'=>'active','workspace_ids'=>[],'password_hash'=>password_hash($password,PASSWORD_DEFAULT),'points'=>0,'stars'=>0,'streak'=>0,'last_completed_date'=>'','created_at'=>app_now()->format(DateTimeInterface::ATOM),'profile_photo'=>'','profile_background'=>'','two_factor_enabled'=>false,'two_factor_secret'=>'','two_factor_pending_secret'=>'','terms_accepted_at'=>app_now()->format(DateTimeInterface::ATOM)];
        $store['users'][]=$user;
        if($familyHubMode === 'join' && $targetWorkspace){
            $workspaceId = (string)$targetWorkspace['id'];
            link_user_to_family_hub_in_store($store, $userId, $workspaceId, $workspaceRole);
            write_store($store);
            $_SESSION['user_id']=$userId; $_SESSION['active_workspace_id']=$workspaceId;
            flash('success','Welcome. Your ' . ($accountType === 'parent' ? 'parent/admin' : 'child/member') . ' account joined ' . ($targetWorkspace['name'] ?? 'the selected Family Hub') . '.'); redirect_to('dashboard');
        }
        $workspaceName = $familyHubName !== '' ? $familyHubName : ($name . "'s Family Hub");
        $workspaceId = create_private_workspace_for_user($store, $userId, $workspaceName, $workspaceRole);
        write_store($store);
        $_SESSION['user_id']=$userId; $_SESSION['active_workspace_id']=$workspaceId;
        seed_demo_items($workspaceId);
        flash('success','Welcome. Your new Family Hub was created. Your parent account is ready for approval and review.'); redirect_to('dashboard');
    }
    if($action==='login'){
        $email=trim((string)($_POST['email'] ?? '')); $password=(string)($_POST['password'] ?? ''); $user=user_by_email($store,$email);
        $rememberLogin = !empty($_POST['remember_credentials']);
        if(!$user || ($user['status'] ?? 'active') !== 'active' || !password_verify($password,$user['password_hash'] ?? '')){ flash('error','Invalid login details, or this account is archived.'); redirect_to('login'); }
        if(!empty($user['two_factor_enabled'])){ $_SESSION['pending_2fa_user_id']=$user['id']; $_SESSION['pending_2fa_started_at']=time(); $_SESSION['pending_remember_credentials']=$rememberLogin; flash('info','Enter the 6-digit code from your authenticator app.'); redirect_to('login'); }
        $_SESSION['user_id']=$user['id']; $_SESSION['active_workspace_id']=current_workspace_id_for_user($user);
        if ($rememberLogin) issue_remember_login_token($store, (string)$user['id']);
        else clear_current_remember_login($store);
        write_store($store);
        flash('success','Signed in successfully.'); redirect_to('dashboard');
    }
    if($action==='logout'){ clear_current_remember_login($store); write_store($store); session_destroy(); header('Location: index.php?page=login'); exit; }
    $user=require_user(); $hubId=hub_id_for_user($user); $return=preg_replace('/[^a-z_]/','',(string)($_POST['return'] ?? 'dashboard')) ?: 'dashboard';

    if($action==='save_my_profile'){ update_current_user_profile($user, $_POST); redirect_to($return); }
    if($action==='change_password'){ update_current_user_password($user, $_POST); redirect_to('settings'); }
    if($action==='start_2fa_setup'){ start_2fa_setup($user); redirect_to('settings'); }
    if($action==='confirm_2fa_setup'){ confirm_2fa_setup($user, $_POST); redirect_to('settings'); }
    if($action==='disable_2fa'){ disable_2fa($user, $_POST); redirect_to('settings'); }
    if($action==='toggle_theme'){
        $ts=settings_for($hubId);
        $targetTheme = (string)($_POST['theme_target'] ?? '');
        if (in_array($targetTheme, ['dark','light'], true)) $ts['theme'] = $targetTheme;
        else $ts['theme']=(($ts['theme'] ?? 'dark') === 'light') ? 'dark' : 'light';
        save_settings($hubId,$ts); redirect_to($return);
    }
    if($action==='toggle_view_mode'){ $ts=settings_for($hubId); $current=$ts['view_mode'] ?? 'auto'; $ts['view_mode']= $current === 'mobile' ? 'desktop' : ($current === 'desktop' ? 'auto' : 'mobile'); save_settings($hubId,$ts); redirect_to($return); }
    if($action==='superadmin_update_user_name' || $action==='superadmin_update_user_identity'){ superadmin_update_user_identity($user, $_POST); redirect_to('admin'); }

    if($action==='switch_workspace'){ set_active_workspace_for_current_user((string)($_POST['workspace_id'] ?? '')); redirect_to($return); }
    if($action==='join_workspace'){
        $joinWorkspaceId = trim((string)($_POST['family_hub_id'] ?? $_POST['workspace_id'] ?? ''));
        if($joinWorkspaceId !== '') join_workspace_by_id($user, $joinWorkspaceId);
        else join_workspace_by_code($user, (string)($_POST['family_hub_code'] ?? $_POST['join_code'] ?? ''));
        redirect_to('workspaces');
    }
    if($action==='create_personal_workspace'){ create_personal_family_hub($user, (string)($_POST['family_hub_name'] ?? $_POST['workspace_name'] ?? '')); redirect_to('workspaces'); }
    if($action==='leave_workspace'){ leave_workspace($user, (string)($_POST['workspace_id'] ?? $hubId)); redirect_to('workspaces'); }
    if($action==='create_workspace_super'){ create_workspace_as_super_admin($user, $_POST); redirect_to('admin'); }
    if($action==='archive_workspace'){ archive_or_restore_workspace($user, (string)($_POST['workspace_id'] ?? ''), 'archived'); redirect_to('admin'); }
    if($action==='restore_workspace'){ archive_or_restore_workspace($user, (string)($_POST['workspace_id'] ?? ''), 'active'); redirect_to('admin'); }
    if($action==='delete_workspace'){ delete_workspace_permanently($user, (string)($_POST['workspace_id'] ?? '')); redirect_to('admin'); }
    if($action==='update_workspace_member_role'){ update_workspace_member_role($user, $_POST); redirect_to($return); }
    if($action==='archive_workspace_member'){ archive_or_restore_workspace_member($user, $_POST, 'archived'); redirect_to($return); }
    if($action==='restore_workspace_member'){ archive_or_restore_workspace_member($user, $_POST, 'active'); redirect_to($return); }
    if($action==='archive_user'){ archive_or_restore_user($user, (string)($_POST['target_user_id'] ?? ''), 'archived'); redirect_to('admin'); }
    if($action==='restore_user'){ archive_or_restore_user($user, (string)($_POST['target_user_id'] ?? ''), 'active'); redirect_to('admin'); }

    if($action==='ai_create_item'){ ai_create_item_response($user, $hubId, $_POST); }
    if($action==='ai_update_reminder'){ ai_update_item_reminder_response($user, $hubId, $_POST); }
    if($action==='ai_complete_item'){ ai_complete_item_response($user, $hubId, $_POST); }
    if($action==='ai_chat_upload'){ ai_chat_upload_response($user, $hubId, $_POST); }

    if($action==='create_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot add new calendar items.'); redirect_to($return); } create_item($hubId,$_POST); redirect_to($return); }
    if($action==='complete_item'){ if(!can_complete_items($user)){ permission_denied('This workspace role cannot complete items.'); redirect_to($return); } complete_item($user['id'],$hubId,(string)($_POST['item_id'] ?? '')); redirect_to($return); }
    if($action==='complete_item_with_proof'){ if(!can_complete_items($user)){ permission_denied('This workspace role cannot complete items.'); redirect_to($return); } complete_item_with_proof($user,$hubId,(string)($_POST['item_id'] ?? '')); redirect_to($return); }
    if($action==='approve_homework_proof'){ approve_homework_proof($user,$hubId,(string)($_POST['item_id'] ?? '')); redirect_to($return); }
    if($action==='request_homework_correction'){ request_homework_correction($user,$hubId,(string)($_POST['item_id'] ?? ''),(string)($_POST['parent_comment'] ?? '')); redirect_to($return); }
    if($action==='delete_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot delete items.'); redirect_to($return); } delete_item($hubId,(string)($_POST['item_id'] ?? '')); redirect_to($return); }
    if($action==='create_list_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot add list items.'); redirect_to($return); } create_list_item($hubId,$_POST); redirect_to($return); }
    if($action==='toggle_list_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot update shared lists.'); redirect_to($return); } toggle_list_item($hubId,(string)($_POST['list_id'] ?? '')); redirect_to($return); }
    if($action==='delete_list_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot delete shared list items.'); redirect_to($return); } delete_list_item($hubId,(string)($_POST['list_id'] ?? '')); redirect_to($return); }
    if($action==='save_weekly_meal_plan'){ if(!can_edit_items($user)){ permission_denied('Your role cannot save meal plans.'); redirect_to('meals'); } save_weekly_meal_plan($hubId,$_POST); redirect_to('meals', ['meal_view'=>'weekly']); }
    if($action==='add_menu_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot add menu items.'); redirect_to('meals'); } add_menu_item($hubId,$_POST); redirect_to('meals', ['meal_view'=>'library']); }
    if($action==='toggle_menu_favorite'){ if(!can_edit_items($user)){ permission_denied('Your role cannot update favorite meals.'); redirect_to('meals'); } toggle_menu_favorite($hubId,(string)($_POST['menu_id'] ?? '')); redirect_to('meals', ['meal_view'=>(string)($_POST['meal_view'] ?? 'library')]); }
    if($action==='delete_menu_item'){ if(!can_edit_items($user)){ permission_denied('Your role cannot delete custom menu items.'); redirect_to('meals'); } delete_custom_menu_item($hubId,(string)($_POST['menu_id'] ?? '')); redirect_to('meals', ['meal_view'=>'library']); }
    if($action==='save_connection'){ if(!can_manage_sync($user)){ permission_denied('Calendar sync records require workspace Admin access.'); redirect_to('sync'); } save_connection($hubId,$_POST); redirect_to('sync'); }
    if($action==='delete_connection'){ if(!can_manage_sync($user)){ permission_denied('Calendar sync records require workspace Admin access.'); redirect_to('sync'); } delete_connection($hubId,(string)($_POST['connection_id'] ?? '')); redirect_to('sync'); }
    if($action==='create_share_invite'){ flash('info','Family Hub access is now managed on the Family Hubs page.'); redirect_to('workspaces'); }
    if($action==='revoke_share'){ flash('info','Family Hub access is managed locally. Super Admin can archive the Family Hub if access must be disabled.'); redirect_to('workspaces'); }
    if($action==='add_photo'){ if(!can_manage_family_settings($user)){ permission_denied('Display screensaver cards require workspace Admin access.'); redirect_to('display'); } add_photo_card($hubId,$_POST); redirect_to('display'); }
    if($action==='magic_import'){ if(!can_edit_items($user)){ permission_denied('Magic Import requires workspace access.'); redirect_to('magic'); } magic_import($hubId,(string)($_POST['import_text'] ?? '')); redirect_to('magic'); }
    if($action==='sidekick_generate'){ if(!can_edit_items($user)){ permission_denied('Sidekick meal generation requires workspace access.'); redirect_to('magic'); } sidekick_generate($hubId,$_POST); redirect_to('magic'); }
    if($action==='update_user_role'){ flash('info','System roles are protected. Use workspace member role controls instead.'); redirect_to('admin'); }
    if($action==='save_settings'){
        if(!can_manage_family_settings($user)){ permission_denied('Settings require workspace Admin access.'); redirect_to('settings'); }
        $settings=settings_for($hubId);
        $settings['theme']=in_array($_POST['theme'] ?? '', ['dark','light','auto'], true) ? $_POST['theme'] : 'dark';
        $settings['view_mode']=in_array($_POST['view_mode'] ?? '', ['auto','mobile','desktop'], true) ? $_POST['view_mode'] : 'auto';
        $settings['accent']=in_array($_POST['accent'] ?? '', ['ocean','violet','sunset','forest','rose'], true) ? $_POST['accent'] : 'ocean';
        $settings['daily_goal']=max(10,min(200,(int)($_POST['daily_goal'] ?? 40))); $settings['reminder_minutes']=max(0,min(1440,(int)($_POST['reminder_minutes'] ?? 15)));
        $settings['home_message']=trim((string)($_POST['home_message'] ?? '')) ?: 'The family command center is ready.';
        $settings['weather_location']=trim((string)($_POST['weather_location'] ?? 'Singapore')) ?: 'Singapore'; $settings['weather_summary']=trim((string)($_POST['weather_summary'] ?? 'Partly cloudy • 31°C')) ?: 'Partly cloudy • 31°C';
        $settings['sleep_start']=trim((string)($_POST['sleep_start'] ?? '22:30')) ?: '22:30'; $settings['sleep_end']=trim((string)($_POST['sleep_end'] ?? '06:30')) ?: '06:30'; $settings['sleep_enabled']=isset($_POST['sleep_enabled']); $settings['parental_lock']=isset($_POST['parental_lock']); $settings['screensaver_enabled']=isset($_POST['screensaver_enabled']); $settings['device_name']=trim((string)($_POST['device_name'] ?? 'Kitchen Display')) ?: 'Kitchen Display'; $settings['device_unlock_enabled']=isset($_POST['device_unlock_enabled']); $settings['device_unlock_minutes']=max(5,min(1440,(int)($_POST['device_unlock_minutes'] ?? 120))); $settings['device_unlock_webhook_url']=trim((string)($_POST['device_unlock_webhook_url'] ?? ''));
        $profiles=[]; $names=$_POST['profile_name'] ?? []; $colors=$_POST['profile_color'] ?? []; $avatars=$_POST['profile_avatar'] ?? [];
        if(is_array($names)) foreach($names as $idx=>$name){ $name=trim((string)$name); if($name==='') continue; $color=trim((string)($colors[$idx] ?? profile_color($name))); if(!preg_match('/^#[0-9a-fA-F]{6}$/',$color)) $color=profile_color($name); $profiles[]=['name'=>$name,'color'=>$color,'avatar'=>trim((string)($avatars[$idx] ?? '👤')) ?: '👤']; }
        $settings['profiles']=$profiles ?: default_profiles(); save_settings($hubId,$settings); flash('success','Settings saved.'); redirect_to('settings');
    }
}

?>
