package com.dangerousthings.aliro;

import javacard.framework.Util;

/**
 * Builds the mdoc DeviceResponse (ISO 18013-5 §8.3.2.1.2.2 / Aliro Table 8-22)
 * carrying the cached Access Document at
 * {@code documents[0].issuerSigned.issuerAuth}.
 *
 * <p>The fixed shape (Aliro Step-Up only ever returns a single mDL document
 * with empty nameSpaces and no deviceSigned):
 * <pre>
 *   DeviceResponse = {
 *       "status":    0,
 *       "version":   "1.0",
 *       "documents": [
 *           {
 *               "docType":      "org.iso.18013.5.1.mDL",
 *               "issuerSigned": {
 *                   "issuerAuth": &lt;Access Document bytes verbatim&gt;,
 *                   "nameSpaces": {}
 *               }
 *           }
 *       ]
 *   }
 * </pre>
 *
 * <p>Map keys are canonical-CBOR sorted (RFC 8949 §4.2.1: encoded-key-length
 * then lexicographic). Both 10-char issuerSigned keys tie on length; 'i' &lt; 'n'
 * so issuerAuth precedes nameSpaces.
 *
 * <p>The Access Document is embedded VERBATIM via
 * {@link Util#arrayCopyNonAtomic} — it was verified as a 4-element COSE_Sign1
 * at personalization (see {@code CoseVerifier}), so the applet trusts its
 * encoding and must not re-encode it (re-encoding could alter the very bytes
 * the issuer signed over).
 *
 * <p>All map / array headers are single-byte for our fixed counts
 * ({@code map(0)=0xA0, map(2)=0xA2, map(3)=0xA3, array(1)=0x81}) and inlined
 * — extending {@link StructuralCbor} with encodeMapHeader / encodeArrayHeader
 * would add a static helper used only here. Tstr key/value bodies go through
 * {@link StructuralCbor#encodeTstrHeader} so the header / payload split stays
 * canonical-CBOR-correct (1-byte header for these tiny strings, but the
 * encoder picks the right size on its own).
 *
 * @see CoseVerifier for the personalization-time AD shape check this builder
 *      relies on.
 */
final class DeviceResponseBuilder {

    // Canonical CBOR map / array headers used by the fixed Aliro DeviceResponse
    // shape. All entry counts are < 24, so each fits in the immediate
    // 1-byte form (major type bits 7..5, additional info bits 4..0).
    private static final byte CBOR_MAP_3   = (byte) 0xA3; // map, 3 entries
    private static final byte CBOR_MAP_2   = (byte) 0xA2; // map, 2 entries
    private static final byte CBOR_MAP_0   = (byte) 0xA0; // map, 0 entries (empty)
    private static final byte CBOR_ARRAY_1 = (byte) 0x81; // array, 1 element

    // Top-level DeviceResponse keys, sorted canonical (length-then-lex).
    private static final byte[] KEY_STATUS    = { 's', 't', 'a', 't', 'u', 's' };
    private static final byte[] KEY_VERSION   = { 'v', 'e', 'r', 's', 'i', 'o', 'n' };
    private static final byte[] KEY_DOCUMENTS = { 'd', 'o', 'c', 'u', 'm', 'e', 'n', 't', 's' };

    // Document entry keys.
    private static final byte[] KEY_DOC_TYPE      = { 'd', 'o', 'c', 'T', 'y', 'p', 'e' };
    private static final byte[] KEY_ISSUER_SIGNED = {
        'i', 's', 's', 'u', 'e', 'r', 'S', 'i', 'g', 'n', 'e', 'd' };

    // issuerSigned entry keys — issuerAuth before nameSpaces ('i' < 'n').
    private static final byte[] KEY_ISSUER_AUTH = {
        'i', 's', 's', 'u', 'e', 'r', 'A', 'u', 't', 'h' };
    private static final byte[] KEY_NAME_SPACES = {
        'n', 'a', 'm', 'e', 'S', 'p', 'a', 'c', 'e', 's' };

    // Constants for the only docType Aliro Step-Up ever emits.
    private static final byte[] VAL_VERSION = { '1', '.', '0' };
    private static final byte[] VAL_MDL_DOC_TYPE = {
        'o', 'r', 'g', '.', 'i', 's', 'o', '.', '1', '8', '0', '1', '3',
        '.', '5', '.', '1', '.', 'm', 'D', 'L' };

    private DeviceResponseBuilder() {
        // Utility class — no instances.
    }

    /**
     * Builds a canonical-CBOR DeviceResponse carrying {@code ad} at
     * {@code documents[0].issuerSigned.issuerAuth}. Writes into {@code out}
     * starting at {@code outOff} and returns the total bytes written.
     *
     * <p>For a 272-byte AD the output is 372 bytes (100 B wrapper); the
     * caller should provision {@code out} with at least {@code adLen + 128}
     * bytes for headroom against future shape tweaks. This builder is
     * allocation-free and side-effect-free; the only state it touches is
     * the slice {@code out[outOff..outOff+return)}.
     *
     * @param ad     Access Document buffer (treated as a single CBOR blob,
     *               embedded verbatim — caller has already validated shape)
     * @param adOff  AD start offset
     * @param adLen  AD length
     * @param out    output buffer
     * @param outOff output start offset
     * @return total bytes written
     */
    static short build(
            byte[] ad, short adOff, short adLen,
            byte[] out, short outOff) {

        short p = outOff;

        // DeviceResponse map header — 3 entries: status, version, documents.
        out[p++] = CBOR_MAP_3;

        // "status": 0
        p += StructuralCbor.encodeTstrHeader((short) KEY_STATUS.length, out, p);
        p = arrayCopy(KEY_STATUS, out, p);
        p += StructuralCbor.encodeUint(0, out, p);

        // "version": "1.0"
        p += StructuralCbor.encodeTstrHeader((short) KEY_VERSION.length, out, p);
        p = arrayCopy(KEY_VERSION, out, p);
        p += StructuralCbor.encodeTstrHeader((short) VAL_VERSION.length, out, p);
        p = arrayCopy(VAL_VERSION, out, p);

        // "documents": [ <document> ]
        p += StructuralCbor.encodeTstrHeader((short) KEY_DOCUMENTS.length, out, p);
        p = arrayCopy(KEY_DOCUMENTS, out, p);
        out[p++] = CBOR_ARRAY_1;

        // document map header — 2 entries: docType, issuerSigned.
        out[p++] = CBOR_MAP_2;

        // "docType": "org.iso.18013.5.1.mDL"
        p += StructuralCbor.encodeTstrHeader((short) KEY_DOC_TYPE.length, out, p);
        p = arrayCopy(KEY_DOC_TYPE, out, p);
        p += StructuralCbor.encodeTstrHeader((short) VAL_MDL_DOC_TYPE.length, out, p);
        p = arrayCopy(VAL_MDL_DOC_TYPE, out, p);

        // "issuerSigned": { ... }
        p += StructuralCbor.encodeTstrHeader((short) KEY_ISSUER_SIGNED.length, out, p);
        p = arrayCopy(KEY_ISSUER_SIGNED, out, p);

        // issuerSigned map header — 2 entries: issuerAuth, nameSpaces.
        out[p++] = CBOR_MAP_2;

        // "issuerAuth": <AD bytes verbatim>
        p += StructuralCbor.encodeTstrHeader((short) KEY_ISSUER_AUTH.length, out, p);
        p = arrayCopy(KEY_ISSUER_AUTH, out, p);
        Util.arrayCopyNonAtomic(ad, adOff, out, p, adLen);
        p = (short) (p + adLen);

        // "nameSpaces": {}
        p += StructuralCbor.encodeTstrHeader((short) KEY_NAME_SPACES.length, out, p);
        p = arrayCopy(KEY_NAME_SPACES, out, p);
        out[p++] = CBOR_MAP_0;

        return (short) (p - outOff);
    }

    /** Copy a small constant byte[] into {@code out[off..)} and return the new write cursor. */
    private static short arrayCopy(byte[] src, byte[] out, short off) {
        Util.arrayCopyNonAtomic(src, (short) 0, out, off, (short) src.length);
        return (short) (off + src.length);
    }
}
