package com.dangerousthings.aliro;

/**
 * Shared, package-private provisioning store. Holds the Access Credential
 * private key and (eventually) reader public keys. The
 * {@link PersonalizationApplet} writes into it; {@link AliroApplet} reads
 * from it when it needs long-term keys for AUTH1 / step-up.
 *
 * <p>Ownership: the single live instance is owned by the
 * {@link PersonalizationApplet} instance (created in its constructor via
 * {@link #bootstrap()}). The static {@link #INSTANCE} field is just an
 * in-package publish-point so AliroApplet/StepUpApplet can find it via
 * {@link #get()} without going through a {@code Shareable} SIO.
 *
 * <p>The on-instance ownership matters for GlobalPlatform Amendment H
 * (Executable Load File Upgrade): AMD-H preserves registered applet
 * instances and their reachable object graph but wipes static fields. With
 * the live reference held on the PersonalizationApplet instance, an upgrade
 * keeps enrollment data; PersonalizationApplet's restore hook then calls
 * {@link #republish(CredentialStore)} to re-establish the static alias.
 *
 * <p>After {@link #commit()} is called, further writes via the
 * personalization interface are refused. The only way to re-unlock is
 * a factory reset, which is not yet implemented.
 */
final class CredentialStore {

    static final short CRED_PRIV_KEY_LEN = 32;
    static final short CRED_PUBK_LEN = 64;
    static final short READER_PUBK_LEN = 64;
    static final short CRED_ISSUER_PUBK_LEN = 64;

    /** Max Access Document blob size (serialized COSE_Sign1 bytes). 1 KB
     *  accommodates a typical Aliro Access Document with room to spare. */
    static final short ACCESS_DOC_MAX_LEN = 1024;

    /** Stable field order for AMD-H Element serialization. Append-only —
     *  never reorder or remove without bumping the package version and
     *  writing an explicit migration step in the new ELF's onRestore.
     *
     *  <p>v3 (M2A.3): adds {@code credentialIssuerPubKey} (64 B x||y) +
     *  {@code credentialIssuerPubKeySet} flag for IssuerAuth verify at
     *  finalize. */
    static final byte FIELD_VERSION = 3;

    /** Publish-point read by AliroApplet/StepUpApplet via {@link #get()}.
     *  PersonalizationApplet owns the actual instance; this is just an alias
     *  so other applets in the same package can find it without SIO. The
     *  reference is re-published after an AMD-H restore (see
     *  PersonalizationApplet.onRestore). */
    private static CredentialStore INSTANCE;

    private final byte[] credentialPrivKey;
    private boolean credentialPrivKeySet;

    private final byte[] credentialPubKey;
    private boolean credentialPubKeySet;

    private final byte[] readerPubKey;
    private boolean readerPubKeySet;

    private final byte[] credentialIssuerPubKey;
    private boolean credentialIssuerPubKeySet;

    private final byte[] accessDocument;
    private short accessDocumentLen;
    private boolean accessDocumentFinalized;
    private boolean accessDocumentVerified;

    private boolean committed;

    private CredentialStore() {
        credentialPrivKey = new byte[CRED_PRIV_KEY_LEN];
        credentialPubKey = new byte[CRED_PUBK_LEN];
        readerPubKey = new byte[READER_PUBK_LEN];
        credentialIssuerPubKey = new byte[CRED_ISSUER_PUBK_LEN];
        accessDocument = new byte[ACCESS_DOC_MAX_LEN];
    }

    /** Called once from {@link PersonalizationApplet}'s constructor. */
    static CredentialStore bootstrap() {
        INSTANCE = new CredentialStore();
        return INSTANCE;
    }

    /** Called by {@code PersonalizationApplet.onRestore} to re-publish a
     *  restored store after an AMD-H Executable Load File upgrade, where
     *  static fields are wiped but the PersonalizationApplet instance (and
     *  its CredentialStore reference) survive. */
    static void republish(CredentialStore restored) {
        INSTANCE = restored;
    }

    static CredentialStore get() {
        return INSTANCE;
    }

    boolean isCommitted() {
        return committed;
    }

    void commit() {
        committed = true;
    }

    /**
     * Returns true iff a credential private key has been written. Callers
     * that need the actual bytes should use {@link #copyCredentialPrivKey}.
     */
    boolean hasCredentialPrivKey() {
        return credentialPrivKeySet;
    }

    void setCredentialPrivKey(byte[] src, short off) {
        javacard.framework.Util.arrayCopyNonAtomic(
            src, off, credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN);
        credentialPrivKeySet = true;
    }

    short copyCredentialPrivKey(byte[] dst, short dstOff) {
        javacard.framework.Util.arrayCopyNonAtomic(
            credentialPrivKey, (short) 0, dst, dstOff, CRED_PRIV_KEY_LEN);
        return CRED_PRIV_KEY_LEN;
    }

    boolean hasCredentialPubKey() {
        return credentialPubKeySet;
    }

    void setCredentialPubKey(byte[] src, short off) {
        javacard.framework.Util.arrayCopyNonAtomic(
            src, off, credentialPubKey, (short) 0, CRED_PUBK_LEN);
        credentialPubKeySet = true;
    }

    short copyCredentialPubKey(byte[] dst, short dstOff) {
        javacard.framework.Util.arrayCopyNonAtomic(
            credentialPubKey, (short) 0, dst, dstOff, CRED_PUBK_LEN);
        return CRED_PUBK_LEN;
    }

    /** Copies only the 32-byte x-coordinate of credential_PubK (spec uses x-only in salt_volatile). */
    short copyCredentialPubKeyX(byte[] dst, short dstOff) {
        javacard.framework.Util.arrayCopyNonAtomic(
            credentialPubKey, (short) 0, dst, dstOff, (short) 32);
        return (short) 32;
    }

    boolean hasReaderPubKey() {
        return readerPubKeySet;
    }

    void setReaderPubKey(byte[] src, short off) {
        javacard.framework.Util.arrayCopyNonAtomic(
            src, off, readerPubKey, (short) 0, READER_PUBK_LEN);
        readerPubKeySet = true;
    }

    short copyReaderPubKey(byte[] dst, short dstOff) {
        javacard.framework.Util.arrayCopyNonAtomic(
            readerPubKey, (short) 0, dst, dstOff, READER_PUBK_LEN);
        return READER_PUBK_LEN;
    }

    /** Copies only the 32-byte x-coordinate of the reader_PubK. */
    short copyReaderPubKeyX(byte[] dst, short dstOff) {
        javacard.framework.Util.arrayCopyNonAtomic(
            readerPubKey, (short) 0, dst, dstOff, (short) 32);
        return (short) 32;
    }

    boolean hasCredentialIssuerPubKey() {
        return credentialIssuerPubKeySet;
    }

    /** Writes the 64-byte x||y issuer public key (no 0x04 prefix). */
    void setCredentialIssuerPubKey(byte[] src, short off) {
        javacard.framework.Util.arrayCopyNonAtomic(
            src, off, credentialIssuerPubKey, (short) 0, CRED_ISSUER_PUBK_LEN);
        credentialIssuerPubKeySet = true;
    }

    /**
     * Emits the credential issuer public key as 65-byte SEC1 uncompressed
     * point (0x04 || X || Y) into {@code dst[dstOff..dstOff+65)}. This is
     * the shape {@link CoseVerifier#verifyCoseSign1} expects for its issuer
     * pubkey parameter.
     */
    short copyCredentialIssuerPubKeyUncomp(byte[] dst, short dstOff) {
        dst[dstOff] = (byte) 0x04;
        javacard.framework.Util.arrayCopyNonAtomic(
            credentialIssuerPubKey, (short) 0, dst, (short) (dstOff + 1), CRED_ISSUER_PUBK_LEN);
        return (short) (CRED_ISSUER_PUBK_LEN + 1);
    }

    /**
     * Stores an Access Document chunk at the given destination offset.
     * Returns true iff the write stayed within {@link #ACCESS_DOC_MAX_LEN};
     * otherwise the store is unchanged.
     */
    boolean writeAccessDocumentChunk(byte[] src, short srcOff, short dstOff, short len) {
        if ((short) (dstOff + len) > ACCESS_DOC_MAX_LEN || dstOff < 0 || len < 0) {
            return false;
        }
        javacard.framework.Util.arrayCopyNonAtomic(src, srcOff, accessDocument, dstOff, len);
        return true;
    }

    /**
     * Finalizes the Access Document: runs the supplied {@link CoseVerifier}
     * against the staged AD bytes using the stored Credential Issuer public
     * key. On success, atomically sets {@code accessDocumentLen},
     * {@code accessDocumentFinalized}, and {@code accessDocumentVerified}
     * inside a {@link javacard.framework.JCSystem#beginTransaction} so
     * partial state can never ship.
     *
     * <p>Pre-conditions enforced by the caller (PersonalizationApplet):
     * <ul>
     *   <li>The issuer pubkey must be set ({@link #hasCredentialIssuerPubKey}) —
     *     callers that omit this get SW_CONDITIONS_NOT_SATISFIED.</li>
     * </ul>
     *
     * <p>This is the M2A.3 expansion of opt 4a from the Step-Up plan: caching
     * the verify result saves ~100 ms per Step-Up transaction. The
     * verify-flag write and the {@code accessDocumentFinalized} flip land in
     * the same atomic transaction so partial state can't ship an unverified
     * document marked verified.
     *
     * @param totalLen   length of staged AD bytes at {@code accessDocument[0..)}
     * @param verifier   IssuerAuth verifier (held as a field on
     *                   PersonalizationApplet; never per-call constructed)
     * @param scratch65  caller-supplied 65 B scratch into which this method
     *                   writes the uncompressed issuer pubkey before handing
     *                   it to {@code verifier.verifyCoseSign1}
     * @return true iff verify succeeded AND state was atomically committed;
     *         false on bad length OR verify failure (state untouched).
     */
    boolean finalizeAccessDocument(short totalLen, CoseVerifier verifier, byte[] scratch65) {
        if (totalLen < 0 || totalLen > ACCESS_DOC_MAX_LEN) {
            return false;
        }
        // Load the issuer pubkey into the caller-supplied scratch as
        // 0x04 || x || y (CoseVerifier needs SEC1 uncompressed).
        copyCredentialIssuerPubKeyUncomp(scratch65, (short) 0);

        boolean verified = verifier.verifyCoseSign1(
            accessDocument, (short) 0, totalLen, scratch65, (short) 0);
        if (!verified) {
            return false;
        }

        try {
            javacard.framework.JCSystem.beginTransaction();
            accessDocumentLen = totalLen;
            accessDocumentFinalized = true;
            accessDocumentVerified = true;
            javacard.framework.JCSystem.commitTransaction();
        } catch (Throwable t) {
            if (javacard.framework.JCSystem.getTransactionDepth() != 0) {
                javacard.framework.JCSystem.abortTransaction();
            }
            return false;
        }
        return true;
    }

    boolean hasAccessDocument() {
        return accessDocumentFinalized;
    }

    /**
     * Test-only: forces the verified flag without running the IssuerAuth
     * COSE_Sign1 verify. Used by tests that exercise downstream behavior
     * (signaling bitmap, serialization round-trip) with opaque AD bytes that
     * aren't signed by a real issuer key. Production code reaches the verified
     * state via {@link #finalizeAccessDocument(short, CoseVerifier, byte[])}.
     */
    void markAccessDocumentVerifiedForTesting() {
        accessDocumentVerified = true;
    }

    boolean isAccessDocumentVerified() {
        return accessDocumentVerified;
    }

    short getAccessDocumentLen() {
        return accessDocumentLen;
    }

    short copyAccessDocument(byte[] dst, short dstOff, short srcOff, short len) {
        javacard.framework.Util.arrayCopyNonAtomic(accessDocument, srcOff, dst, dstOff, len);
        return len;
    }

    /**
     * Writes the full credential store state to the given sink in
     * {@link #FIELD_VERSION} layout. The field order is locked — see the
     * FIELD_VERSION javadoc.
     */
    void writeTo(SerializationSink sink) {
        sink.write(FIELD_VERSION);
        sink.write(committed);
        sink.write(credentialPrivKeySet);
        sink.write(credentialPubKeySet);
        sink.write(readerPubKeySet);
        sink.write(credentialIssuerPubKeySet);
        sink.write(accessDocumentFinalized);
        sink.write(accessDocumentVerified);
        sink.write(accessDocumentLen);
        sink.write(credentialPrivKey);
        sink.write(credentialPubKey);
        sink.write(readerPubKey);
        sink.write(credentialIssuerPubKey);
        sink.write(accessDocument);
    }

    /**
     * Reads a full credential store snapshot from the given source written
     * by {@link #writeTo(SerializationSink)}.
     */
    static CredentialStore readFrom(SerializationSource src) {
        byte v = src.readByte();
        if (v != FIELD_VERSION) {
            javacard.framework.ISOException.throwIt(
                javacard.framework.ISO7816.SW_DATA_INVALID);
        }
        CredentialStore s = new CredentialStore();
        s.committed = src.readBoolean();
        s.credentialPrivKeySet = src.readBoolean();
        s.credentialPubKeySet = src.readBoolean();
        s.readerPubKeySet = src.readBoolean();
        s.credentialIssuerPubKeySet = src.readBoolean();
        s.accessDocumentFinalized = src.readBoolean();
        s.accessDocumentVerified = src.readBoolean();
        s.accessDocumentLen = src.readShort();
        byte[] a;
        a = src.readByteArray();
        javacard.framework.Util.arrayCopy(a, (short) 0, s.credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN);
        a = src.readByteArray();
        javacard.framework.Util.arrayCopy(a, (short) 0, s.credentialPubKey, (short) 0, CRED_PUBK_LEN);
        a = src.readByteArray();
        javacard.framework.Util.arrayCopy(a, (short) 0, s.readerPubKey, (short) 0, READER_PUBK_LEN);
        a = src.readByteArray();
        javacard.framework.Util.arrayCopy(a, (short) 0, s.credentialIssuerPubKey, (short) 0, CRED_ISSUER_PUBK_LEN);
        a = src.readByteArray();
        javacard.framework.Util.arrayCopy(a, (short) 0, s.accessDocument, (short) 0, ACCESS_DOC_MAX_LEN);
        return s;
    }

    /**
     * Test-only: marks the staged AD bytes as finalized without running the
     * IssuerAuth verify. Used by AliroApplet tests that exercise downstream
     * behavior (signaling bitmap etc.) with opaque AD bytes that aren't
     * signed by a real issuer key.
     */
    void markAccessDocumentFinalizedForTesting(short totalLen) {
        accessDocumentLen = totalLen;
        accessDocumentFinalized = true;
    }

    /** Test-only: returns a fresh byte[] copy of the credential private key. */
    byte[] copyCredentialPrivKey() {
        byte[] out = new byte[CRED_PRIV_KEY_LEN];
        javacard.framework.Util.arrayCopyNonAtomic(
            credentialPrivKey, (short) 0, out, (short) 0, CRED_PRIV_KEY_LEN);
        return out;
    }

    /** Test-only: returns a fresh byte[] copy of the credential public key. */
    byte[] copyCredentialPubKey() {
        byte[] out = new byte[CRED_PUBK_LEN];
        javacard.framework.Util.arrayCopyNonAtomic(
            credentialPubKey, (short) 0, out, (short) 0, CRED_PUBK_LEN);
        return out;
    }

    /** Test-only: returns a fresh byte[] copy of the reader public key. */
    byte[] copyReaderPubKey() {
        byte[] out = new byte[READER_PUBK_LEN];
        javacard.framework.Util.arrayCopyNonAtomic(
            readerPubKey, (short) 0, out, (short) 0, READER_PUBK_LEN);
        return out;
    }

    /** Test-only: returns a fresh byte[] copy of the active Access Document
     *  bytes (length = {@link #accessDocumentLen}, not the full buffer). */
    byte[] copyAccessDocument() {
        byte[] out = new byte[accessDocumentLen];
        javacard.framework.Util.arrayCopyNonAtomic(
            accessDocument, (short) 0, out, (short) 0, accessDocumentLen);
        return out;
    }

    /**
     * Test-only hook. The store is a static singleton, which makes unit
     * tests interfere with each other unless each test starts from a
     * known-blank slate. Package-private and named for clarity.
     */
    void resetForTesting() {
        javacard.framework.Util.arrayFillNonAtomic(credentialPrivKey, (short) 0, CRED_PRIV_KEY_LEN, (byte) 0);
        javacard.framework.Util.arrayFillNonAtomic(credentialPubKey, (short) 0, CRED_PUBK_LEN, (byte) 0);
        javacard.framework.Util.arrayFillNonAtomic(readerPubKey, (short) 0, READER_PUBK_LEN, (byte) 0);
        javacard.framework.Util.arrayFillNonAtomic(credentialIssuerPubKey, (short) 0, CRED_ISSUER_PUBK_LEN, (byte) 0);
        javacard.framework.Util.arrayFillNonAtomic(accessDocument, (short) 0, ACCESS_DOC_MAX_LEN, (byte) 0);
        credentialPrivKeySet = false;
        credentialPubKeySet = false;
        readerPubKeySet = false;
        credentialIssuerPubKeySet = false;
        accessDocumentLen = 0;
        accessDocumentFinalized = false;
        accessDocumentVerified = false;
        committed = false;
    }
}
