package invalid.lena.scrcpy;

import android.content.Context;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.TBSCertificate;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import io.github.muntashirakon.adb.AbsAdbConnectionManager;
import io.github.muntashirakon.adb.AdbStream;
import io.github.muntashirakon.adb.LocalServices;

// Thin facade over libadb-android. One instance per process: pairs once,
// connects per session, then exposes typed openers for the three streams
// the rest of the app cares about (shell, sync, localabstract).
//
// The keypair lives in filesDir/{adbkey,adbcert}: PKCS#8 DER for the key,
// X.509 DER for the cert. Software-backed RSA - AndroidKeyStore is unusable
// here because libadb-android's auth path uses raw RSA/ECB/NoPadding which
// hardware-backed keys refuse.
public final class Adb extends AbsAdbConnectionManager {

    private static final String DEVICE_NAME = "scrcpy-android";
    private static final String KEY_FILE = "adbkey";
    private static final String CERT_FILE = "adbcert";

    // Per-operation timeout for adb protocol calls (pair/connect/openStream).
    // libadb-android defaults to Long.MAX_VALUE; a hung target would block
    // forever otherwise.
    private static final long OP_TIMEOUT_MS = 15_000L;

    // libadb-android's mApi is documented as "the target's API for protocol
    // negotiation". We don't know the target's API at construction time;
    // pinning a recent release (API 34) keeps the protocol at the newest
    // version wireless-debugging Android-11+ targets expect. Bump when a
    // newer protocol version ships.
    private static final int  TARGET_API_HINT = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;

    // Singleton: Main and Mirror both want an Adb; building two would
    // race on the on-disk keypair and waste a RSA generation. Lazily
    // initialised on the FIRST caller, which should be off the UI thread.
    private static volatile Adb instance;

    public static Adb getInstance(Context ctx) throws Exception {
        Adb local = instance;
        if (local != null) return local;
        synchronized (Adb.class) {
            if (instance == null) instance = new Adb(ctx.getApplicationContext());
            return instance;
        }
    }

    private final PrivateKey privateKey;
    private final Certificate certificate;

    private Adb(Context ctx) throws Exception {
        setApi(TARGET_API_HINT);
        setTimeout(OP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        File keyFile = new File(ctx.getFilesDir(), KEY_FILE);
        File certFile = new File(ctx.getFilesDir(), CERT_FILE);
        if (keyFile.exists() && certFile.exists()) {
            privateKey = loadKey(keyFile);
            certificate = loadCert(certFile);
            Log.i("adb: loaded keypair from %s", keyFile);
        } else {
            KeyPair kp = generateRsa();
            privateKey = kp.getPrivate();
            certificate = selfSignedCert(kp);
            saveKey(keyFile, privateKey);
            saveCert(certFile, certificate);
            Log.i("adb: generated new keypair at %s", keyFile);
        }
    }

    @Override protected PrivateKey getPrivateKey()  { return privateKey; }
    @Override protected Certificate getCertificate() { return certificate; }
    @Override protected String      getDeviceName() { return DEVICE_NAME; }

    // ---- typed openers ----

    public AdbStream openAbstract(String name) throws IOException, InterruptedException {
        return openStream(LocalServices.LOCAL_UNIX_SOCKET_ABSTRACT, name);
    }

    public AdbStream openShell(String cmd) throws IOException, InterruptedException {
        // Bypass LocalServices.getDestination(SHELL, args) - it wraps any
        // arg containing a space in literal double quotes, and adbd then
        // tries to exec `"the whole quoted thing"` as a single filename.
        // Build the destination ourselves so the cmd reaches sh -c
        // unmolested.
        return openStream("shell:" + cmd);
    }

    public AdbStream openSync() throws IOException, InterruptedException {
        return openStream(LocalServices.SYNC);
    }

    // ---- key + cert I/O ----

    private static KeyPair generateRsa() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048, new SecureRandom());
        return gen.generateKeyPair();
    }

    private static PrivateKey loadKey(File f) throws Exception {
        byte[] data = Files.readAllBytes(f.toPath());
        return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(data));
    }

    private static Certificate loadCert(File f) throws Exception {
        try (InputStream in = new FileInputStream(f)) {
            return CertificateFactory.getInstance("X.509").generateCertificate(in);
        }
    }

    private static void saveKey(File f, PrivateKey k) throws IOException {
        AtomicFiles.write(f, k.getEncoded());
    }

    private static void saveCert(File f, Certificate c) throws Exception {
        AtomicFiles.write(f, c.getEncoded());
    }

    // Build a minimal self-signed X.509 certificate over the keypair.
    //
    // ASN.1 layout (RFC 5280 sec. 4.1):
    //
    //   Certificate ::= SEQUENCE {
    //     tbsCertificate       TBSCertificate,
    //     signatureAlgorithm   AlgorithmIdentifier,    -- sha256WithRSAEnc
    //     signature            BIT STRING               -- RSA over tbs
    //   }
    //
    //   TBSCertificate ::= SEQUENCE {
    //     version              [0] EXPLICIT v3,
    //     serialNumber         INTEGER 1,
    //     signatureAlgorithm   AlgorithmIdentifier,
    //     issuer  = subject    Name (CN=scrcpy-android),
    //     validity             { notBefore, notAfter },
    //     subjectPublicKeyInfo SubjectPublicKeyInfo
    //   }
    //
    // ADB validates the peer by the raw RSA public-key fingerprint in the
    // SPKI bits, not by the DN or signature, so the surrounding cert is
    // cosmetic - but a valid X.509 wrapper is still required for the TLS
    // handshake that wireless-debugging uses post-pairing.
    private static Certificate selfSignedCert(KeyPair kp) throws Exception {
        long now = System.currentTimeMillis();
        Date notBefore = new Date(now - 60_000L);
        Date notAfter  = new Date(now + 50L * 365 * 24 * 3600 * 1000L);
        X500Name dn = new X500Name("CN=" + DEVICE_NAME);
        AlgorithmIdentifier sigAlg =
                new AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption);

        V3TBSCertificateGenerator g = new V3TBSCertificateGenerator();
        g.setSerialNumber(new ASN1Integer(BigInteger.ONE));
        g.setSignature(sigAlg);
        g.setIssuer(dn);
        g.setSubject(dn);
        g.setStartDate(new Time(notBefore));
        g.setEndDate(new Time(notAfter));
        g.setSubjectPublicKeyInfo(
                SubjectPublicKeyInfo.getInstance(kp.getPublic().getEncoded()));

        TBSCertificate tbs = g.generateTBSCertificate();

        Signature s = Signature.getInstance("SHA256withRSA");
        s.initSign(kp.getPrivate());
        s.update(tbs.getEncoded(ASN1Encoding.DER));
        byte[] sig = s.sign();

        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(tbs);
        v.add(sigAlg);
        v.add(new DERBitString(sig));
        byte[] certDer = new DERSequence(v).getEncoded(ASN1Encoding.DER);

        return CertificateFactory.getInstance("X.509")
                .generateCertificate(new ByteArrayInputStream(certDer));
    }
}
