package com.dangerousthings.aliro;

import javacard.framework.APDU;
import javacard.framework.Applet;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;

/**
 * Receives Access Credential long-term private key, reader public keys,
 * and (later) the pre-signed Access Document bytes. Accepts writes only
 * until {@link CredentialStore#commit()}; afterwards every write returns
 * SW_CONDITIONS_NOT_SATISFIED.
 *
 * <p>This applet lives under {@link AliroAids#PROVISIONING}, a
 * DangerousThings-proprietary AID — it is not part of the Aliro spec
 * surface.
 */
public class PersonalizationApplet extends Applet {

    private static final byte CLA = (byte) 0x80;

    private static final byte INS_SET_CREDENTIAL_PRIV_KEY = (byte) 0x20;
    private static final byte INS_SET_CREDENTIAL_PUBK = (byte) 0x21;
    private static final byte INS_SET_READER_PUBK = (byte) 0x22;
    private static final byte INS_WRITE_ACCESS_DOC = (byte) 0x23;
    private static final byte INS_FINALIZE_ACCESS_DOC = (byte) 0x24;
    private static final byte INS_SET_CREDENTIAL_ISSUER_PUBK = (byte) 0x25;
    private static final byte INS_COMMIT = (byte) 0x2C;

    /** The owning reference to the shared CredentialStore. AliroApplet and
     *  StepUpApplet reach it via the {@link CredentialStore#get()} static
     *  publish-point; we keep the actual reference on this applet instance
     *  so AMD-H Executable Load File upgrades (which wipe static fields but
     *  preserve registered applet instances and their reachable object
     *  state) don't lose enrollment data. */
    private final CredentialStore store;

    /** One-shot IssuerAuth COSE_Sign1 verifier — constructed at install so
     *  the 768 B CLEAR_ON_DESELECT transient scratch is allocated once.
     *  Reconstructing it on every finalize would eventually exhaust the
     *  transient pool on real hardware. */
    private final CoseVerifier issuerVerifier;

    /** Persistent 65 B scratch the issuer pubkey is rebuilt into before
     *  every {@link CoseVerifier#verifyCoseSign1} call. Held on the applet
     *  instance so it survives AMD-H upgrades — we'd rather pay the 65 B
     *  of EEPROM than risk transient-pool exhaustion or per-finalize
     *  allocation. */
    private final byte[] issuerPubScratch;

    public PersonalizationApplet() {
        this.store = CredentialStore.bootstrap();
        this.issuerVerifier = new CoseVerifier();
        this.issuerPubScratch = new byte[65];
    }

    public static void install(byte[] bArray, short bOffset, byte bLength) {
        PersonalizationApplet applet = new PersonalizationApplet();
        if (bArray == null || bLength == 0) {
            applet.register();
        } else {
            applet.register(bArray, (short) (bOffset + 1), bArray[bOffset]);
        }
    }

    @Override
    public void process(APDU apdu) {
        if (selectingApplet()) {
            return;
        }

        byte[] buf = apdu.getBuffer();
        if (buf[ISO7816.OFFSET_CLA] != CLA) {
            ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
        }

        // Use the instance-owned reference rather than the static publish-point
        // so this applet keeps working even before/around an AMD-H restore.
        CredentialStore store = this.store;
        byte ins = buf[ISO7816.OFFSET_INS];

        switch (ins) {
            case INS_SET_CREDENTIAL_PRIV_KEY:
                requireUnlocked(store);
                setCredentialPrivKey(apdu, store);
                return;
            case INS_SET_CREDENTIAL_PUBK:
                requireUnlocked(store);
                setCredentialPubKey(apdu, store);
                return;
            case INS_SET_READER_PUBK:
                requireUnlocked(store);
                setReaderPubKey(apdu, store);
                return;
            case INS_SET_CREDENTIAL_ISSUER_PUBK:
                requireUnlocked(store);
                setCredentialIssuerPubKey(apdu, store);
                return;
            case INS_WRITE_ACCESS_DOC:
                requireUnlocked(store);
                writeAccessDocChunk(apdu, store);
                return;
            case INS_FINALIZE_ACCESS_DOC:
                requireUnlocked(store);
                finalizeAccessDoc(apdu, store);
                return;
            case INS_COMMIT:
                requireUnlocked(store);
                store.commit();
                return;
            default:
                ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
        }
    }

    private static void requireUnlocked(CredentialStore store) {
        if (store.isCommitted()) {
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }
    }

    private void setCredentialPrivKey(APDU apdu, CredentialStore store) {
        short lc = apdu.setIncomingAndReceive();
        if (lc != CredentialStore.CRED_PRIV_KEY_LEN) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        byte[] buf = apdu.getBuffer();
        store.setCredentialPrivKey(buf, apdu.getOffsetCdata());
    }

    private void setCredentialPubKey(APDU apdu, CredentialStore store) {
        short lc = apdu.setIncomingAndReceive();
        if (lc != CredentialStore.CRED_PUBK_LEN) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        byte[] buf = apdu.getBuffer();
        store.setCredentialPubKey(buf, apdu.getOffsetCdata());
    }

    private void setReaderPubKey(APDU apdu, CredentialStore store) {
        short lc = apdu.setIncomingAndReceive();
        if (lc != CredentialStore.READER_PUBK_LEN) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        byte[] buf = apdu.getBuffer();
        store.setReaderPubKey(buf, apdu.getOffsetCdata());
    }

    private void setCredentialIssuerPubKey(APDU apdu, CredentialStore store) {
        short lc = apdu.setIncomingAndReceive();
        if (lc != CredentialStore.CRED_ISSUER_PUBK_LEN) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        byte[] buf = apdu.getBuffer();
        store.setCredentialIssuerPubKey(buf, apdu.getOffsetCdata());
    }

    /**
     * Writes an Access Document chunk. P1|P2 is the destination offset
     * within the stored Access Document (big-endian unsigned 16-bit); the
     * command data field is the chunk bytes.
     */
    private void writeAccessDocChunk(APDU apdu, CredentialStore store) {
        short lc = apdu.setIncomingAndReceive();
        byte[] buf = apdu.getBuffer();
        short dstOff = (short) (((buf[ISO7816.OFFSET_P1] & 0xFF) << 8)
                              |  (buf[ISO7816.OFFSET_P2] & 0xFF));
        boolean ok = store.writeAccessDocumentChunk(
            buf, apdu.getOffsetCdata(), dstOff, lc);
        if (!ok) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
    }

    /**
     * Finalizes the Access Document: triggers a one-shot IssuerAuth verify
     * against the stored Credential Issuer pubkey and atomically flips
     * {@code accessDocumentFinalized} + {@code accessDocumentVerified} on
     * success. P1|P2 is the total length (big-endian unsigned 16-bit).
     * Lc must be 0.
     *
     * <p>Pre-conditions: the issuer pubkey must have been set first
     * (INS_SET_CREDENTIAL_ISSUER_PUBK = 0x25); without it we have no trust
     * anchor and reject with {@code SW_CONDITIONS_NOT_SATISFIED}. A verify
     * failure (signature didn't match) returns {@code SW_DATA_INVALID}.
     */
    private void finalizeAccessDoc(APDU apdu, CredentialStore store) {
        short lc = apdu.setIncomingAndReceive();
        if (lc != 0) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        if (!store.hasCredentialIssuerPubKey()) {
            ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
        }
        byte[] buf = apdu.getBuffer();
        short totalLen = (short) (((buf[ISO7816.OFFSET_P1] & 0xFF) << 8)
                                |  (buf[ISO7816.OFFSET_P2] & 0xFF));
        // Bound-check first so the existing oversize test stays at
        // SW_WRONG_DATA. Once length is valid, only IssuerAuth verify can
        // fail — and that's the tamper case the brief maps to SW_DATA_INVALID.
        if (totalLen < 0 || totalLen > CredentialStore.ACCESS_DOC_MAX_LEN) {
            ISOException.throwIt(ISO7816.SW_WRONG_DATA);
        }
        if (!store.finalizeAccessDocument(totalLen, issuerVerifier, issuerPubScratch)) {
            ISOException.throwIt(ISO7816.SW_DATA_INVALID);
        }
    }
}
