package com.foxx.androidcast;

/*********************************************************************
 * DeviceInfo.java
 * Created at: Sat 16 May 2026 13:35:48 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class DeviceInfo
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 135 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	b71e7b9724e505fccce802ab5ea188575fbbb3e33d978f19dcbc5a11e1b8dacb
**********************************************************************/
import android.content.Context;
import android.content.SharedPreferences;
import android.media.MediaDrm;
import android.os.Build;
import android.provider.Settings;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.UUID;

/**
 * Stable device identity for cast handshakes. Survives app reinstall when possible
 * (Widevine ID + per-app ANDROID_ID hash). Cached in {@link SharedPreferences}.
 */
public final class DeviceInfo {
  private static final String PREFS = "androidcast_device";
  private static final String KEY_UUID = "device_uuid";
  private static final UUID WIDEVINE_UUID =
      UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");

  private static volatile String cachedUuid;

  private DeviceInfo() {}

  /** Returns a stable UUID string for this device (app-singleton). */
  public static String getDeviceUuid(Context context) {
    if (cachedUuid != null) {
      return cachedUuid;
    }
    synchronized (DeviceInfo.class) {
      if (cachedUuid != null) {
        return cachedUuid;
      }
      Context app = context.getApplicationContext();
      SharedPreferences prefs = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
      String stored = prefs.getString(KEY_UUID, null);
      if (stored != null && !stored.isEmpty()) {
        cachedUuid = stored;
        return cachedUuid;
      }
      String generated = generateStableUuid(app);
      prefs.edit().putString(KEY_UUID, generated).apply();
      cachedUuid = generated;
      return cachedUuid;
    }
  }

  /** Lower 32 bits of hashed UUID for {@code placeholder_id} in proto headers. */
  public static int getPlaceholderId(Context context) {
    return (int) (hashUuid(getDeviceUuid(context)) & 0xFFFFFFFFL);
  }

  private static String generateStableUuid(Context context) {
    StringBuilder material = new StringBuilder();
    material.append("androidcast:");
    String androidId = Settings.Secure.getString(
        context.getContentResolver(), Settings.Secure.ANDROID_ID);
    if (androidId != null && !androidId.isEmpty()) {
      material.append(androidId);
    }
    byte[] widevine = readWidevineId();
    if (widevine != null && widevine.length > 0) {
      material.append(':');
      for (byte b : widevine) {
        material.append(String.format(Locale.US, "%02x", b));
      }
    }
    material.append(':').append(Build.BOARD).append(':').append(Build.DEVICE);
    return uuidFromSha256(material.toString());
  }

  private static byte[] readWidevineId() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
      return null;
    }
    MediaDrm drm = null;
    try {
      drm = new MediaDrm(WIDEVINE_UUID);
      return drm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
    } catch (Exception ignored) {
      return null;
    } finally {
      if (drm != null) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
          drm.close();
        } else {
          drm.release();
        }
      }
    }
  }

  private static String uuidFromSha256(String material) {
    try {
      MessageDigest md = MessageDigest.getInstance("SHA-256");
      byte[] digest = md.digest(material.getBytes(StandardCharsets.UTF_8));
      long msb = 0;
      long lsb = 0;
      for (int i = 0; i < 8; i++) {
        msb = (msb << 8) | (digest[i] & 0xff);
      }
      for (int i = 8; i < 16; i++) {
        lsb = (lsb << 8) | (digest[i] & 0xff);
      }
      return new UUID(msb, lsb).toString();
    } catch (NoSuchAlgorithmException e) {
      return UUID.randomUUID().toString();
    }
  }

  private static long hashUuid(String uuid) {
    try {
      MessageDigest md = MessageDigest.getInstance("SHA-256");
      byte[] digest = md.digest(uuid.getBytes(StandardCharsets.UTF_8));
      long h = 0;
      for (int i = 0; i < 8; i++) {
        h = (h << 8) | (digest[i] & 0xff);
      }
      return h;
    } catch (NoSuchAlgorithmException e) {
      return uuid.hashCode();
    }
  }
}
