<?php
/**
 * /api/device/* — IBO Player Pro style pairing.
 *
 * v1.3.0 update — the Android client now sends its real hardware identity
 * in the registration body so the user no longer sees the placeholder
 * "00:00:00:00:00:00" MAC.
 *
 * Registration body (all optional — empty body still works for browsers):
 *   {
 *     "mac_address" : "AA:BB:CC:DD:EE:FF",   // real WiFi/eth MAC if Android could read it
 *     "hardware_id" : "abc123...",            // Settings.Secure.ANDROID_ID — always present on Android
 *     "model"       : "Samsung SM-G991U"      // optional, for the admin panel
 *   }
 *
 * Resolution priority for the stored mac_address column:
 *   1. mac_address  — only if it looks like a real, non-placeholder MAC
 *   2. hardware_id  — formatted as "locally-administered" MAC so legacy UI
 *                     that expects MAC format keeps working
 *   3. gen_pseudo_mac() — last resort (browser previews, missing fields)
 *
 * Duplicate prevention — if the same physical device re-registers (same
 * hardware_id), we return the existing row instead of inserting a new
 * one. If the device hadn't paired yet we hand back its existing
 * (unused) PIN so the QR code stays consistent across app restarts.
 */

function gen_pin(): string {
    return str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
}

function gen_device_id(): string {
    return strtoupper(bin2hex(random_bytes(8)));
}

function gen_pseudo_mac(): string {
    $b = random_bytes(6);
    $b[0] = chr((ord($b[0]) | 0x02) & 0xFE);
    $hex = bin2hex($b);
    return strtoupper(implode(':', str_split($hex, 2)));
}

function unique_pin(): string {
    for ($i = 0; $i < 30; $i++) {
        $pin = gen_pin();
        $stmt = db()->prepare('SELECT 1 FROM devices WHERE pin = ?');
        $stmt->execute([$pin]);
        if (!$stmt->fetch()) return $pin;
    }
    json_error(500, 'Could not allocate unique PIN');
}

/**
 * Make sure the `devices` table has the `hardware_id` column. Older
 * deployments (1.2.x) didn't have it; this is an idempotent migration
 * that runs once per PHP process.
 */
function ensure_devices_hardware_id_column(): void {
    static $done = false;
    if ($done) return;
    $done = true;
    try {
        $exists = db()->query(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME   = 'devices'
               AND COLUMN_NAME  = 'hardware_id'"
        )->fetchColumn();
        if ((int)$exists === 0) {
            db()->exec("ALTER TABLE `devices`
                        ADD COLUMN `hardware_id` VARCHAR(64) DEFAULT NULL,
                        ADD INDEX `ix_devices_hardware_id` (`hardware_id`)");
        }
        // Same for `model` (purely cosmetic for the admin panel).
        $existsModel = db()->query(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME   = 'devices'
               AND COLUMN_NAME  = 'model'"
        )->fetchColumn();
        if ((int)$existsModel === 0) {
            db()->exec("ALTER TABLE `devices` ADD COLUMN `model` VARCHAR(128) DEFAULT NULL");
        }
    } catch (Throwable $e) {
        // Don't fail registration just because we can't migrate; the
        // rest of the flow degrades gracefully (it just won't dedupe).
        @error_log('[device] ensure_devices_hardware_id_column: ' . $e->getMessage());
    }
}

/**
 * Returns the input only if it looks like a real, usable MAC address.
 * Rejects empty strings, the all-zero placeholder and the
 * "anonymised" 02:00:00:00:00:00 that Android 6+ returns when the
 * caller doesn't have ACCESS_FINE_LOCATION.
 */
function sanitize_real_mac($raw): ?string {
    if (!is_string($raw)) return null;
    $s = strtoupper(trim($raw));
    if ($s === '') return null;
    if (!preg_match('/^([0-9A-F]{2}[:\-]){5}[0-9A-F]{2}$/', $s)) return null;
    $normalised = str_replace('-', ':', $s);
    if ($normalised === '00:00:00:00:00:00') return null;
    if ($normalised === '02:00:00:00:00:00') return null;
    return $normalised;
}

/**
 * Best-effort string sanitiser for hardware_id / model. Strips control
 * characters and clamps length so a malicious client can't blow up the
 * row.
 */
function sanitize_short_string($raw, int $maxLen = 64): ?string {
    if (!is_string($raw)) return null;
    $s = trim($raw);
    if ($s === '') return null;
    // Keep printable ASCII + common unicode; drop control bytes.
    $s = preg_replace('/[\x00-\x1F\x7F]/u', '', $s);
    if ($s === null || $s === '') return null;
    if (function_exists('mb_substr')) {
        $s = mb_substr($s, 0, $maxLen);
    } else {
        $s = substr($s, 0, $maxLen);
    }
    return $s;
}

/**
 * Derive a stable, MAC-formatted string from a hardware_id so legacy
 * UI that expects "XX:XX:XX:XX:XX:XX" keeps rendering correctly even
 * when Android only handed us an ANDROID_ID. The locally-administered
 * bit is set so we never collide with a real OUI.
 */
function mac_from_hardware_id(string $hwId): string {
    $hash = strtoupper(substr(sha1($hwId), 0, 12));
    $first = hexdec(substr($hash, 0, 2));
    $first = ($first | 0x02) & 0xFE;
    $bytes = [
        sprintf('%02X', $first),
        substr($hash, 2, 2),
        substr($hash, 4, 2),
        substr($hash, 6, 2),
        substr($hash, 8, 2),
        substr($hash, 10, 2),
    ];
    return implode(':', $bytes);
}

function route_device_register(): void {
    ensure_schema();
    ensure_devices_hardware_id_column();

    // Body is optional — older clients (v1.2.x) just POST nothing.
    $body = [];
    $raw = file_get_contents('php://input');
    if ($raw !== '' && $raw !== false) {
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) $body = $decoded;
    }

    $client_mac    = sanitize_real_mac($body['mac_address']  ?? null);
    $hardware_id   = sanitize_short_string($body['hardware_id'] ?? null, 64);
    $model         = sanitize_short_string($body['model']      ?? null, 128);

    // ── Duplicate-prevention: same physical device re-registering ──
    // If we have a hardware_id and we've seen it before, return the
    // existing row instead of allocating a new device_id / PIN.
    if ($hardware_id !== null) {
        $stmt = db()->prepare(
            'SELECT device_id, mac_address, pin, paired, profile_name, created_at
             FROM devices WHERE hardware_id = ? LIMIT 1'
        );
        $stmt->execute([$hardware_id]);
        $existing = $stmt->fetch();
        if ($existing) {
            // Refresh model / mac if the client now reports a better one.
            $newMac = $client_mac ?? $existing['mac_address'];
            $up = db()->prepare(
                'UPDATE devices SET mac_address = ?, model = COALESCE(?, model)
                 WHERE device_id = ?'
            );
            $up->execute([$newMac, $model, $existing['device_id']]);

            send_json([
                'device_id'    => $existing['device_id'],
                'mac_address'  => $newMac,
                'pin'          => $existing['pin'],
                'paired'       => (bool)$existing['paired'],
                'profile_name' => $existing['profile_name'],
                'created_at'   => $existing['created_at'],
                'reused'       => true,
            ]);
        }
    }

    // ── Brand-new registration ──────────────────────────────────────
    $device_id = gen_device_id();
    $pin       = unique_pin();
    $now       = now_iso();

    if ($client_mac !== null) {
        $mac_address = $client_mac;
    } elseif ($hardware_id !== null) {
        $mac_address = mac_from_hardware_id($hardware_id);
    } else {
        $mac_address = gen_pseudo_mac();
    }

    $stmt = db()->prepare(
        'INSERT INTO devices (device_id, mac_address, pin, paired, created_at, hardware_id, model)
         VALUES (?, ?, ?, 0, ?, ?, ?)'
    );
    $stmt->execute([$device_id, $mac_address, $pin, $now, $hardware_id, $model]);

    send_json([
        'device_id'   => $device_id,
        'mac_address' => $mac_address,
        'pin'         => $pin,
        'paired'      => false,
        'created_at'  => $now,
        'reused'      => false,
    ]);
}

function route_device_status(string $device_id): void {
    $stmt = db()->prepare('SELECT device_id, mac_address, paired, profile_name FROM devices WHERE device_id = ?');
    $stmt->execute([$device_id]);
    $d = $stmt->fetch();
    if (!$d) json_error(404, 'Device not found');
    send_json([
        'device_id'    => $d['device_id'],
        'mac_address'  => $d['mac_address'],
        'paired'       => (bool)$d['paired'],
        'profile_name' => $d['profile_name'],
    ]);
}

function route_device_pair(): void {
    $b = read_json_body();
    $pin = trim((string)require_field($b, 'pin'));
    if (!preg_match('/^\d{6}$/', $pin)) json_error(400, 'PIN must be 6 digits');

    $stmt = db()->prepare("SELECT * FROM devices WHERE pin = ? AND paired = 0");
    $stmt->execute([$pin]);
    $d = $stmt->fetch();
    if (!$d) json_error(404, 'Invalid PIN or device already paired');

    $up = db()->prepare(
        'UPDATE devices
         SET paired = 1, server_url = ?, username = ?, password = ?, profile_name = ?, paired_at = ?
         WHERE device_id = ?'
    );
    $up->execute([
        require_field($b, 'server_url'),
        require_field($b, 'username'),
        require_field($b, 'password'),
        $b['profile_name'] ?? 'Paired Device',
        now_iso(),
        $d['device_id'],
    ]);

    send_json(['success' => true, 'device_id' => $d['device_id'], 'message' => 'Device paired successfully']);
}

function route_device_config(string $device_id): void {
    $stmt = db()->prepare('SELECT * FROM devices WHERE device_id = ? AND paired = 1');
    $stmt->execute([$device_id]);
    $d = $stmt->fetch();
    if (!$d) json_error(404, 'Device not found or not yet paired');
    send_json([
        'device_id'    => $d['device_id'],
        'mac_address'  => $d['mac_address'],
        'server_url'   => $d['server_url'] ?: '',
        'username'     => $d['username']   ?: '',
        'password'     => $d['password']   ?: '',
        'profile_name' => $d['profile_name'] ?: '',
        'paired_at'    => $d['paired_at']  ?: '',
    ]);
}

function route_device_unpair(string $device_id): void {
    $sel = db()->prepare('SELECT 1 FROM devices WHERE device_id = ?');
    $sel->execute([$device_id]);
    if (!$sel->fetch()) json_error(404, 'Device not found');

    $new_pin = unique_pin();
    $up = db()->prepare(
        'UPDATE devices
         SET paired = 0, server_url = NULL, username = NULL, password = NULL,
             profile_name = NULL, paired_at = NULL, pin = ?
         WHERE device_id = ?'
    );
    $up->execute([$new_pin, $device_id]);
    send_json(['success' => true, 'device_id' => $device_id, 'new_pin' => $new_pin]);
}
