<?php
// =============================================================================
// /api/addaplaylist.php — addaplaylist.com integration endpoint.
//
// addaplaylist.com is the third-party QR-pair broker. End-users scan our
// device QR with their phone, land on addaplaylist.com, pick a portal +
// enter their Xtream credentials, and addaplaylist forwards the result here.
//
// Contract (defined by addaplaylist's panel.js / submit handler, must match
// the Smarters V5 "index.php" contract every other registered panel speaks):
//
//   GET  ?action=get_dns       headers: x-auth-token: <auth_token>
//        → 200 {"dns_servers":[{"id":1,"name":"...","url":"http://..."}, ...]}
//
//   POST { payload, iv, tag, hmac }   headers: x-auth-token: <auth_token>
//        → 200 {"status":"ok"}
//
//   Encryption (mirrors addaplaylist's encryptPayload + verifyHmac):
//     key    = SHA256(auth_token)                  raw 32 bytes
//     plain  = JSON.stringify(envelope)
//     cipher = AES-256-GCM(key, iv12) → ciphertext + tag16
//     payload= base64(ciphertext)
//     iv     = base64(iv12)
//     tag    = base64(tag16)
//     hmac   = HMAC-SHA256(key, payload_base64).hex
//
//   Decrypted envelope shape:
//     { action:'submit_credentials', device_code, dns_id, dns_url,
//       username, password, timestamp, nonce }
//
// The INSERT mirrors /add.php exactly so the app's /has_playlist poll picks
// up the new row with zero behavior change. See add.php for the canonical
// shape of a playlists row.
// =============================================================================

require_once __DIR__ . '/../includes/client_common.php';
require_once __DIR__ . '/../includes/db.php';

// ---------------------------------------------------------------------------
// Auth token gate — common to both GET and POST. Constant-time compare so a
// timing-oracle can't bisect the token.
// ---------------------------------------------------------------------------
$cfg = db_config();
$auth_token = (string)($cfg['addaplaylist_auth_token'] ?? '');
if ($auth_token === '') {
    aapl_err('panel not registered with addaplaylist', 404);
}

$supplied = trim((string)($_SERVER['HTTP_X_AUTH_TOKEN'] ?? ''));
if ($supplied === '' || !hash_equals($auth_token, $supplied)) {
    aapl_err('forbidden', 403);
}

$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));

// ---------------------------------------------------------------------------
// GET ?action=get_dns  → return the panel's enabled portals.
// ---------------------------------------------------------------------------
if ($method === 'GET') {
    $action = (string)($_GET['action'] ?? '');
    if ($action !== 'get_dns') {
        aapl_err('unknown action', 400);
    }
    $rows = db_all(
        'SELECT id, name, type, url FROM portals WHERE enabled = 1 ORDER BY sort_order ASC, id ASC'
    );
    $dns_servers = [];
    foreach ($rows as $r) {
        $dns_servers[] = [
            'id'   => (int)$r['id'],
            'name' => (string)$r['name'],
            'url'  => (string)$r['url'],
        ];
    }
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['dns_servers' => $dns_servers], JSON_UNESCAPED_SLASHES);
    exit;
}

// ---------------------------------------------------------------------------
// POST encrypted envelope → decrypt, verify, insert playlist row.
// ---------------------------------------------------------------------------
if ($method !== 'POST') {
    aapl_err('method not allowed', 405);
}

$raw = file_get_contents('php://input', false, null, 0, 65536);
$body = json_decode((string)$raw, true);
if (!is_array($body)) {
    aapl_err('invalid envelope', 400);
}
foreach (['payload', 'iv', 'tag', 'hmac'] as $f) {
    if (!isset($body[$f]) || !is_string($body[$f])) {
        aapl_err("missing $f", 400);
    }
}
$payload_b64 = $body['payload'];
$iv_b64      = $body['iv'];
$tag_b64     = $body['tag'];
$hmac_hex    = $body['hmac'];

// 1. HMAC over base64 payload (matches addaplaylist's order — HMAC BEFORE decrypt)
$key = hash('sha256', $auth_token, true);
$expected_hmac = hash_hmac('sha256', $payload_b64, $key);
if (!hash_equals($expected_hmac, $hmac_hex)) {
    aapl_err('integrity failure', 403);
}

// 2. AES-256-GCM decrypt
$iv  = base64_decode($iv_b64, true);
$tag = base64_decode($tag_b64, true);
$ct  = base64_decode($payload_b64, true);
if ($iv === false || $tag === false || $ct === false || strlen($iv) !== 12 || strlen($tag) !== 16) {
    aapl_err('bad envelope binaries', 400);
}
$plain = openssl_decrypt($ct, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) {
    aapl_err('decryption failed', 400);
}
$env = json_decode((string)$plain, true);
if (!is_array($env) || ($env['action'] ?? '') !== 'submit_credentials') {
    aapl_err('invalid action', 400);
}

// 3. Field validation
$device_code = trim((string)($env['device_code'] ?? ''));
$dns_id      = isset($env['dns_id']) ? (int)$env['dns_id'] : 0;
$dns_url     = trim((string)($env['dns_url']  ?? ''));
$username    = trim((string)($env['username'] ?? ''));
$password    = trim((string)($env['password'] ?? ''));
$timestamp   = (int)($env['timestamp']  ?? 0);
$nonce       = trim((string)($env['nonce']    ?? ''));

if (!preg_match('/^[0-9]{4,10}$/', $device_code)) aapl_err('bad device_code', 400);
if ($dns_id <= 0)                                  aapl_err('bad dns_id',      400);
if ($dns_url === '' || strlen($dns_url) > 512)     aapl_err('bad dns_url',     400);
if ($username === '' || strlen($username) > 128)   aapl_err('bad username',    400);
if ($password === '' || strlen($password) > 256)   aapl_err('bad password',    400);
if (abs(time() * 1000 - $timestamp) > 5 * 60_000)  aapl_err('expired',         403);
if (strlen($nonce) < 16)                           aapl_err('bad nonce',       400);

// 4. Replay guard (per device_code, per nonce). Cheap inline table — same
//    panel.db, idempotent ALTER on first hit.
try {
    panel_db()->exec(
        "CREATE TABLE IF NOT EXISTS addaplaylist_nonces (
            nonce TEXT PRIMARY KEY,
            seen_at INTEGER NOT NULL
        )"
    );
    // Prune anything older than 1 hour so the table stays tiny
    panel_db()->prepare('DELETE FROM addaplaylist_nonces WHERE seen_at < ?')
        ->execute([time() - 3600]);
    db_insert(
        'INSERT INTO addaplaylist_nonces (nonce, seen_at) VALUES (?, ?)',
        [$nonce, time()]
    );
} catch (Throwable $e) {
    // Either the nonce already existed (PK collision = replay) or some other
    // DB issue. Treat as replay for safety.
    aapl_err('replay or db error', 403);
}

// 5. Find the device by device_key. The "device_code" in addaplaylist's
//    vocabulary == the IBO panel's "device_key" column (the 6-digit code
//    shown on the device's pairing screen).
$device = db_one('SELECT id, mac_address FROM devices WHERE device_key = ?', [$device_code]);
if (!$device) {
    aapl_err('device not registered', 404);
}

// 6. Resolve the portal — use dns_id but verify dns_url matches so a tampered
//    payload can't pair a device against a portal it didn't pick.
$portal = db_one('SELECT id, name, type, url FROM portals WHERE id = ?', [$dns_id]);
if (!$portal || (string)$portal['url'] !== $dns_url) {
    aapl_err('dns mismatch', 400);
}

// 7. Build the playlist row in EXACTLY the same shape /add.php uses so the
//    app's /has_playlist + read paths see no difference. Allocate the next
//    public_id per-device the same way /add.php does.
$existing = db_all('SELECT public_id FROM playlists WHERE device_id = ?', [(int)$device['id']]);
$max = 0;
foreach ($existing as $r) {
    if (ctype_digit((string)$r['public_id'])) {
        $n = (int)$r['public_id'];
        if ($n > $max) $max = $n;
    }
}
$public_id = (string)($max + 1);

// Xtream playlist URL — same construction the IBO app uses when the user
// types creds into Add Playlist. /add.php's parse-from-querystring fallback
// will continue to work either way, but giving it the full URL keeps the
// app's "type=xtream" rendering happy out of the box.
$portal_url = rtrim((string)$portal['url'], '/');
$playlist_url = $portal_url
    . '/get.php?username=' . rawurlencode($username)
    . '&password=' . rawurlencode($password)
    . '&type=m3u_plus&output=ts';

$playlist_name = (string)$portal['name'] !== '' ? (string)$portal['name'] : $username;
$playlist_type = (string)($portal['type'] ?? 'xtream');

$new_id = db_insert(
    'INSERT INTO playlists (device_id, public_id, name, type, url, username, password,
                            is_protected, sort_order)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
    [(int)$device['id'], $public_id, $playlist_name, $playlist_type, $playlist_url,
     $username, $password, '0', $max + 1]
);

// 8. Mirror /add.php's logging so this lands in the same panel_request_log
//    feed the admin already watches for app-driven adds.
client_log_request(
    (int)$device['id'],
    'POST',
    '/api/addaplaylist',
    client_request_headers(),
    ['source' => 'addaplaylist.com', 'device_code' => $device_code, 'dns_id' => $dns_id],
    200,
    ['status' => 'ok', 'id' => (int)$new_id]
);

header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'ok'], JSON_UNESCAPED_SLASHES);
exit;

// ---------------------------------------------------------------------------
// Local error helper. Same shape across every return path so callers (and
// addaplaylist's logs) see consistent error JSON.
// ---------------------------------------------------------------------------
function aapl_err(string $msg, int $code = 400): void {
    header('Content-Type: application/json; charset=utf-8', true, $code);
    echo json_encode(['error' => $msg], JSON_UNESCAPED_SLASHES);
    exit;
}
