<?php
// =============================================================================
// client_common.php — required by every CLIENT-facing endpoint (the URLs the
// Android app actually calls). Unlike api/_common.php, these endpoints have
// NO admin session, NO CSRF — they're public and identified by the device's
// own credentials (mac_address, device_key, signatures).
//
// What's included:
//   - bootstrap.php (which loads db.php, helpers, etc — but NOT auth/CSRF)
//   - codec.php (decode_envelope)
//   - crypto.php (sig_gs, sig_hm, bootstrap_*)
//   - utility helpers for body decoding, request logging, mac derivation
// =============================================================================

require_once __DIR__ . '/bootstrap.php';
require_once __DIR__ . '/codec.php';
require_once __DIR__ . '/crypto.php';

header('Cache-Control: no-store');

// ----------------------------------------------------------------------------
// Capture request headers in a single array (for logging).
// ----------------------------------------------------------------------------
function client_request_headers() {
    if (function_exists('getallheaders')) {
        $h = getallheaders();
        if (is_array($h)) {
            return $h;
        }
    }
    // CGI fallback
    $out = [];
    foreach ($_SERVER as $k => $v) {
        if (strncmp($k, 'HTTP_', 5) === 0) {
            $name = strtr(ucwords(strtolower(strtr(substr($k, 5), '_', ' '))), ' ', '-');
            $out[$name] = $v;
        }
    }
    return $out;
}

// ----------------------------------------------------------------------------
// Decode the {"data": "..."} envelope into the inner JSON dict, or return
// null if absent / malformed. Use this for POST endpoints.
// ----------------------------------------------------------------------------
function client_decoded_body() {
    $body = json_request_body();
    if (!isset($body['data']) || !is_string($body['data'])) {
        return null;
    }
    return decode_envelope($body['data']);
}

// ----------------------------------------------------------------------------
// Derive a stable, deterministic MAC address string from app_device_id
// (which is base64(android_id)). Returns "xx:xx:xx:xx:xx:xx" lowercase.
//
// Used when the device first hits /auth and doesn't yet have a row.
// ----------------------------------------------------------------------------
function client_derive_mac($app_device_id) {
    $aid = '';
    $decoded = base64_decode((string)$app_device_id, true);
    if ($decoded !== false) {
        $aid = trim($decoded);
    }
    if ($aid === '') {
        $aid = (string)$app_device_id;
    }
    $h = sha1($aid);                // 40 hex chars
    return implode(':', str_split(substr($h, 0, 12), 2));
}

// ----------------------------------------------------------------------------
// Find or create the device row for an /auth call. Returns the device row.
// Idempotent — every subsequent /auth with the same app_device_id touches
// last_seen_at but otherwise no-ops.
// ----------------------------------------------------------------------------
function client_get_or_create_device($app_device_id, $app_type, $app_version, $is_google_pay = 0) {
    $mac = client_derive_mac($app_device_id);
    $now = unix_now();

    // Try existing by mac
    $existing = db_one('SELECT * FROM devices WHERE mac_address = ?', [$mac]);
    if ($existing) {
        db_exec(
            'UPDATE devices
             SET app_device_id = ?, android_id = ?, app_type = ?, app_version = ?,
                 is_google_pay = ?, last_seen_at = ?
             WHERE id = ?',
            [(string)$app_device_id,
             trim((string)base64_decode((string)$app_device_id, true)),
             (string)$app_type, (string)$app_version,
             truthy($is_google_pay), $now, (int)$existing['id']]
        );
        return db_one('SELECT * FROM devices WHERE id = ?', [(int)$existing['id']]);
    }

    // Fresh insert + create paired subscriptions / parent_control rows.
    // device_key is a 6-digit user-visible code (shown in Settings → Device
    // Key). 1M possible values — plenty for the install base, since real
    // auth uses libapksec signature headers, not this key.
    $device_key = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    for ($i = 0; $i < 5; $i++) {
        if (!db_one('SELECT 1 FROM devices WHERE device_key = ?', [$device_key])) break;
        $device_key = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    }
    $id = db_insert(
        'INSERT INTO devices (mac_address, android_id, app_device_id, device_key,
                              app_type, app_version, is_google_pay,
                              first_seen_at, last_seen_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
        [$mac,
         trim((string)base64_decode((string)$app_device_id, true)),
         (string)$app_device_id,
         $device_key,
         (string)$app_type, (string)$app_version,
         truthy($is_google_pay),
         $now, $now]
    );
    db_exec('INSERT INTO subscriptions  (device_id) VALUES (?)', [$id]);
    db_exec('INSERT INTO parent_control (device_id) VALUES (?)', [$id]);
    return db_one('SELECT * FROM devices WHERE id = ?', [$id]);
}

// ----------------------------------------------------------------------------
// Look up a device by mac_address from the inner JSON. Returns null if missing.
// ----------------------------------------------------------------------------
function client_device_by_mac($mac) {
    if (!is_string($mac) || $mac === '') return null;
    return db_one('SELECT * FROM devices WHERE mac_address = ?', [$mac]);
}

// ----------------------------------------------------------------------------
// Scrub headers that would let a log viewer hijack the requester's session.
//   - Cookie:  contains the MPANEL=<session id> for admin requests, or any
//     other session id a client sends.
//   - x-csrf-token: per-session CSRF token. Less load-bearing than the cookie
//     (rotates on session) but still session-bound, so a peeking viewer could
//     replay it during the same session window.
//   - Authorization: any bearer/basic creds.
//
// Replaces values in-place with a "<redacted>" sentinel so the rest of the
// header set stays useful for debugging.
// ----------------------------------------------------------------------------
function scrub_request_headers($headers) {
    if (!is_array($headers)) return $headers;
    $sensitive = ['cookie', 'set-cookie', 'authorization', 'proxy-authorization',
                  'x-csrf-token', 'x-api-key'];
    foreach ($headers as $k => $v) {
        if (in_array(strtolower((string)$k), $sensitive, true)) {
            $headers[$k] = '<redacted>';
        }
    }
    return $headers;
}

// ----------------------------------------------------------------------------
// Cap each request-log table at roughly N rows. Several panel endpoints
// (/has_playlist, /vpns.php, /api/guard/key, /api/guard/bootstrap) are public
// and unauthenticated, so a sustained probe would otherwise fill the shared-
// hosting SQLite file. We don't add a cron requirement — once in ~100 writes
// the logger checks id-range and trims if the table is over the cap.
// ----------------------------------------------------------------------------
function client_log_prune_table($table, $cap = 50000, $headroom = 40000) {
    if (mt_rand(1, 100) !== 1) return;
    $row = db_one("SELECT MAX(id) AS max_id, MIN(id) AS min_id FROM {$table}");
    if (!$row) return;
    $max = (int)($row['max_id'] ?? 0);
    $min = (int)($row['min_id'] ?? 0);
    if ($max - $min > $cap) {
        $cutoff = $max - $headroom;
        db_exec("DELETE FROM {$table} WHERE id < ?", [$cutoff]);
    }
}

// ----------------------------------------------------------------------------
// Log a panel client request to panel_request_log.
//   $headers / $inner_json may be arrays — they get JSON-encoded.
// ----------------------------------------------------------------------------
function client_log_request($device_id, $method, $path, $headers, $inner_json, $resp_status, $resp_body) {
    db_exec(
        'INSERT INTO panel_request_log (device_id, method, path, headers, inner_json,
                                        response_status, response_body, ip_address)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
        [
            $device_id ?: null,
            (string)$method,
            (string)$path,
            is_string($headers) ? $headers : json_encode(scrub_request_headers($headers), JSON_UNESCAPED_SLASHES),
            is_string($inner_json) ? $inner_json : json_encode($inner_json, JSON_UNESCAPED_SLASHES),
            (int)$resp_status,
            is_string($resp_body) ? substr($resp_body, 0, 16384) : substr(json_encode($resp_body), 0, 16384),
            client_ip(),
        ]
    );
    client_log_prune_table('panel_request_log');
}

// ----------------------------------------------------------------------------
// Build the AppInfoModel response for a given device.
//
// Wire format: the response body is { "data": "<json-string>" }, where the
// inner JSON matches the *exact* shape the IBO Ultra Android app expects.
// The schema is reverse-engineered from a real /auth capture and
// stored in includes/_auth_reference.json — we load it as a template and
// override the per-device / per-config fields.
// ----------------------------------------------------------------------------
function build_app_info_model($device) {
    $cfg = db_config();
    $sub = db_one('SELECT * FROM subscriptions  WHERE device_id = ?', [(int)$device['id']]) ?: [];
    $pc  = db_one('SELECT * FROM parent_control WHERE device_id = ?', [(int)$device['id']]) ?: [];

    // Load the on-the-wire template once and reuse for every call.
    static $template = null;
    if ($template === null) {
        $ref_path = __DIR__ . '/_auth_reference.json';
        $raw = is_file($ref_path) ? file_get_contents($ref_path) : '';
        $template = $raw ? json_decode($raw, true) : [];
        if (!is_array($template)) { $template = []; }
    }
    // Deep copy so per-request mutations don't leak.
    $r = json_decode(json_encode($template), true);

    // ---- Per-config branding ----
    $apk_link  = (string)($cfg['apk_link']     ?? '');
    $r['apk_url']     = $apk_link;
    $r['apk_link']    = $apk_link;
    $r['theme']       = (string)($cfg['theme'] ?? '1');

    // Derive panel base URL (used for the webview endpoints below). Prefer
    // the admin-set config row; fall back to the request's own host.
    $base = (string)($cfg['panel_base_url'] ?? '');
    if ($base === '') {
        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host   = $_SERVER['HTTP_HOST'] ?? 'localhost';
        // strip the trailing endpoint segment off REQUEST_URI to get the panel dir
        $dir    = rtrim(dirname((string)($_SERVER['REQUEST_URI'] ?? '/')), '/');
        $base   = $scheme . '://' . $host . $dir;
    }
    $base = rtrim($base, '/');

    // home_url1 = TMDB-style backdrop with title/overview overlay (the IBO Ultra
    // "background_alt" fetch). home_url2 = sports widget. Both always serve
    // from the panel's own /api/webview endpoints; the buyer never edits them.
    // The user-uploaded slideshow lives at /api/webview/slideshow.php and is
    // surfaced separately through the Media Slideshow admin page, not here.
    $r['qr_url']    = (string)($cfg['qr_url']    ?? '') ?: $base . '/api/webview/qr.php?url=';
    // home_url1 = home backdrop (TMDB rotating titles). home_url2 = Sports
    // button widget. Matches the original wire defaults; admin can override either
    // via the config row.
    //
    // Cache-bust query: the panel-side endpoints emit `Cache-Control: max-age=300`,
    // so the Android WebView's HTTP cache holds responses for 5 min. When admin
    // saves a new TMDB key / feed / slideshow, the app would otherwise keep
    // showing the stale render. Stamp the URL with max(config, webview_settings)
    // updated_at so a save flips the URL → WebView forced to refetch.
    $wv_ver = (int)(db_one('SELECT MAX(updated_at) v FROM webview_settings')['v'] ?? 0);
    $cfg_ver = (int)($cfg['updated_at'] ?? 0);
    $bust = '?v=' . max($wv_ver, $cfg_ver);
    // home_url1 resolution order:
    //   1. Explicit config.home_url1 override (App Config → power users)
    //   2. Framed View, if the operator flipped `Use as home backdrop` on the
    //      Framed View page (webview_settings.home_backdrop_source == 'frameview')
    //   3. Default TMDB autoads backdrop
    $backdrop_src = (string)(db_one('SELECT value FROM webview_settings WHERE key=?',
                                    ['home_backdrop_source'])['value'] ?? 'tmdb');
    $default_home_url1 = ($backdrop_src === 'frameview'
        ? $base . '/api/webview/frameview.php'
        : $base . '/api/webview/tmdb/autoads_0.php') . $bust;
    $r['home_url1'] = (string)($cfg['home_url1'] ?? '') ?: $default_home_url1;
    if (array_key_exists('home_url2', $r)) {
        $r['home_url2'] = (string)($cfg['home_url2'] ?? '')
            ?: $base . '/api/webview/sports/sport_top.php' . $bust;
    }
    $note_title = (string)($cfg['noti_title']   ?? 'Welcome');
    $note_body  = (string)($cfg['noti_content'] ?? 'Enjoy your streaming!');
    $r['note_title']   = $note_title;
    $r['note_content'] = $note_body;
    if (isset($r['notification']) && is_array($r['notification'])) {
        $r['notification']['title']   = $note_title;
        $r['notification']['content'] = $note_body;
    }
    $r['app_description']    = (string)($cfg['app_description']    ?? $note_title);
    $r['app_general_player'] = (string)($cfg['app_general_player'] ?? $note_body);
    $r['android_version_code'] = (string)($cfg['app_version'] ?? '56');
    $r['app_version']          = (string)($cfg['app_version'] ?? '56');
    if (isset($r['windows']) && is_array($r['windows'])) {
        $r['windows']['version']   = (string)($cfg['app_version'] ?? '56');
        $r['windows']['downloads'] = [$apk_link];
    }

    // ---- Per-device identity ----
    $r['device_key']  = (string)$device['device_key'];
    $r['mac_address'] = (string)$device['mac_address'];

    // ---- Subscription / trial / lock ----
    $expire_date = (string)($sub['expired_date'] ?? '');
    $r['expire_date'] = $expire_date !== '' ? $expire_date : '2099-01-01 23:59:59';
    $r['is_trial']    = (string)((int)($sub['is_trial'] ?? 0));   // string per wire
    $r['is_expired']  = $expire_date !== '' && strtotime($expire_date) < time();
    $r['is_google_paid'] = (bool)($device['is_google_pay'] ?? 0);
    $r['lock']        = (int)($sub['lock']   ?? 0);
    $r['plan_id']     = (string)($sub['plan_id'] ?? '');
    $r['price']       = (string)($sub['price']   ?? '0.00');

    // ---- Parent control / pin ----
    $r['parent_control'] = (string)($pc['parent_control'] ?? '0000');
    $r['parent_pin']     = (string)($pc['pin_code']       ?? '0000');
    $r['parent_synced']  = (int)($pc['parent_synced']     ?? 1);
    $r['pin']            = (string)($pc['pin_code']       ?? '0000');

    // ---- Portals (from panel admin) ----
    $portals_db = db_all(
        'SELECT id, name, url, is_protected, type
         FROM portals WHERE enabled = 1 ORDER BY sort_order ASC'
    );
    $r['portals'] = array_map(function ($p) {
        return [
            'id'           => (int)$p['id'],
            'name'         => (string)$p['name'],
            'url'          => (string)$p['url'],
            'is_protected' => (string)($p['is_protected'] ?? '0'),
            'type'         => (string)($p['type'] ?? 'general'),
        ];
    }, $portals_db);
    // services mirror portals as {title, url}
    $r['services'] = array_map(function ($p) {
        return ['title' => (string)$p['name'], 'url' => (string)$p['url']];
    }, $portals_db);

    // ---- VPN profiles (from panel admin) ----
    $vpns_db = db_all(
        'SELECT name, provider, country, state, ovpn, username, password
         FROM vpn_profiles WHERE enabled = 1 ORDER BY sort_order ASC'
    );
    if (!empty($vpns_db)) {
        $r['vpns'] = array_map(function ($v) {
            return [
                'name'     => (string)$v['name'],
                'provider' => (string)$v['provider'],
                'country'  => (string)$v['country'],
                'state'    => (string)$v['state'],
                'ovpn'     => (string)$v['ovpn'],
                'username' => (string)$v['username'],
                'password' => (string)$v['password'],
            ];
        }, $vpns_db);
    }

    // ---- Misc per-instance fields ----
    $r['mac_registered'] = true;
    $r['serverTime']     = time();
    $r['status']         = 'success';
    if (isset($r['android_testing']) && is_array($r['android_testing'])) {
        $r['android_testing']['ibo'] = (bool)($cfg['is_ibo'] ?? 0);
        $r['android_testing']['vu']  = (bool)($cfg['is_vu']  ?? 0);
    }

    // ---- Per-device saved playlists ----
    // The app stores any playlists the user has added on THIS MAC and reads
    // them back from the `urls` field on every /auth. Auto-login: a returning
    // device skips the Add Playlist screen because urls[] is non-empty.
    $pls = db_all(
        'SELECT public_id, name, type, url, username, password, is_protected
         FROM playlists WHERE device_id = ? ORDER BY sort_order ASC',
        [(int)$device['id']]
    );
    $r['urls'] = array_map(function ($p) {
        return [
            'id'           => (string)$p['public_id'],
            'name'         => (string)$p['name'],
            'type'         => (string)($p['type'] ?? 'general'),
            'url'          => (string)$p['url'],
            'username'     => (string)($p['username'] ?? ''),
            'password'     => (string)($p['password'] ?? ''),
            'is_protected' => (string)($p['is_protected'] ?? '0'),
        ];
    }, $pls);
    $r['has_own_playlist'] = !empty($pls);

    return $r;
}
