"""In-process Python port of AliroApplet's AUTH0+AUTH1 paths.

Used by test_reader_transaction_offline.py to validate the reader without
needing a real card or pyscard. Mirrors AliroApplet processAuth0 +
processAuth1 -- but only enough to satisfy a one-shot reader transaction.

Skips: provisioning, replay protection (we never call AUTH1 twice in tests),
step-up, mailbox, vendor extensions, error paths beyond happy-path returns.
"""

from dataclasses import dataclass

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
    decode_dss_signature,
    encode_dss_signature,
)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

from aliro_harness.reader.auth1 import (
    build_table_812,
    build_table_813,
)
from aliro_harness.reader.crypto import derive_kdh
from aliro_harness.reader.key_derivation import (
    OFF_EXPEDITED_SK_DEVICE,
    build_salt_volatile,
    derive_expedited_session_keys,
)

# AIDs and INS values -- match the applet
EXPEDITED_AID = bytes.fromhex("A000000909ACCE5501")
INS_SELECT = 0xA4
INS_AUTH0 = 0x80
INS_AUTH1 = 0x81

# FCI bytes the real applet returns from sendExpeditedFci. Pre-computed so
# the fake card returns a byte-identical FCI.
#   6F 15                            -- FCI template, outer len = 21
#     84 09 A0 00 00 09 09 AC CE 55 01   -- DF name = EXPEDITED_AID
#     A5 08                               -- proprietary, inner len = 8
#       80 02 00 00                      -- application type = CSA (0x0000)
#       5C 02 01 00                      -- protocol version = 0x0100
EXPEDITED_FCI = bytes.fromhex(
    "6F15"
    "8409" "A000000909ACCE5501"
    "A508" "80020000" "5C020100"
)

# AUTH0 TLV tags (AliroApplet)
TAG_COMMAND_PARAMETERS = 0x41
TAG_AUTHENTICATION_POLICY = 0x42
TAG_PROTOCOL_VERSION = 0x5C
TAG_READER_EPUBK = 0x87
TAG_TRANSACTION_ID = 0x4C
TAG_READER_IDENTIFIER = 0x4D
TAG_CREDENTIAL_EPUBK = 0x86

# AUTH1 TLV tags
TAG_READER_SIGNATURE = 0x9E
TAG_READER_CERT = 0x90

PROTOCOL_VERSION_1_0 = 0x0100


@dataclass
class _FakeSession:
    """Per-transaction state captured during AUTH0, consumed by AUTH1."""

    reader_ephem_pub_uncompressed: bytes
    reader_group_id: bytes
    reader_group_sub_id: bytes
    transaction_id: bytes
    command_parameters: int
    authentication_policy: int
    credential_ephem_pub_uncompressed: bytes | None = None


@dataclass
class FakeAliroCard:
    """Holds provisioned trust state + per-transaction state.

    Mirrors just enough of AliroApplet to drive the reader through
    SELECT -> AUTH0 -> AUTH1 successfully.
    """

    # Provisioned trust state
    reader_pub: ec.EllipticCurvePublicKey
    credential_priv: ec.EllipticCurvePrivateKey
    credential_pub: ec.EllipticCurvePublicKey
    has_access_document: bool = False

    # Per-transaction state, set during processAuth0
    auth0_done: bool = False
    session: _FakeSession | None = None
    credential_ephem_priv: ec.EllipticCurvePrivateKey | None = None

    @classmethod
    def provisioned(
        cls,
        *,
        reader_pub: ec.EllipticCurvePublicKey,
        credential_priv: ec.EllipticCurvePrivateKey,
        credential_pub: ec.EllipticCurvePublicKey,
        has_access_document: bool = False,
    ) -> "FakeAliroCard":
        return cls(
            reader_pub=reader_pub,
            credential_priv=credential_priv,
            credential_pub=credential_pub,
            has_access_document=has_access_document,
        )

    # ---------------- Transport ----------------

    def transmit(self, apdu: bytes) -> tuple[bytes, int]:
        """Implements ISO 7816 SELECT + AUTH0 + AUTH1 INSes."""
        if len(apdu) < 4:
            return b"", 0x6700
        cla, ins, p1, _p2 = apdu[0], apdu[1], apdu[2], apdu[3]

        # SELECT is CLA=0x00
        if cla == 0x00 and ins == INS_SELECT and p1 == 0x04:
            if len(apdu) < 5:
                return b"", 0x6700
            lc = apdu[4]
            aid = apdu[5 : 5 + lc]
            if aid == EXPEDITED_AID:
                # Fresh SELECT clears any prior session state.
                self.auth0_done = False
                self.session = None
                self.credential_ephem_priv = None
                return EXPEDITED_FCI, 0x9000
            return b"", 0x6A82  # file not found

        if cla == 0x80 and ins == INS_AUTH0:
            return self._process_auth0(apdu)
        if cla == 0x80 and ins == INS_AUTH1:
            return self._process_auth1(apdu)
        if cla != 0x80:
            return b"", 0x6E00  # CLA not supported
        return b"", 0x6D00  # INS not supported

    # ---------------- AUTH0 ----------------

    def _process_auth0(self, apdu: bytes) -> tuple[bytes, int]:
        """Validate AUTH0 TLVs, capture session state, generate ephemeral,
        return 0x86 TLV (uncompressed 65B credential_ePubK)."""
        if len(apdu) < 5:
            return b"", 0x6700
        lc = apdu[4]
        data = apdu[5 : 5 + lc]
        if len(data) != lc:
            return b"", 0x6700

        try:
            parsed = self._parse_auth0(data)
        except _WrongData:
            return b"", 0x6A80

        # Capture session state
        self.session = _FakeSession(
            reader_ephem_pub_uncompressed=parsed["reader_ephem_pub"],
            reader_group_id=parsed["reader_group_id"],
            reader_group_sub_id=parsed["reader_group_sub_id"],
            transaction_id=parsed["transaction_id"],
            command_parameters=parsed["command_parameters"],
            authentication_policy=parsed["authentication_policy"],
        )

        # Generate fresh credential ephemeral keypair
        self.credential_ephem_priv = ec.generate_private_key(ec.SECP256R1())
        ephem_pub_uncompressed = self.credential_ephem_priv.public_key().public_bytes(
            encoding=serialization.Encoding.X962,
            format=serialization.PublicFormat.UncompressedPoint,
        )
        self.session.credential_ephem_pub_uncompressed = ephem_pub_uncompressed

        # Build response: 0x86 0x41 [65B]
        resp = bytes([TAG_CREDENTIAL_EPUBK, len(ephem_pub_uncompressed)]) + ephem_pub_uncompressed
        self.auth0_done = True
        return resp, 0x9000

    def _parse_auth0(self, data: bytes) -> dict:
        if len(data) == 0:
            raise _WrongData()
        seen = 0
        out = {}
        i = 0
        while i < len(data):
            if i + 2 > len(data):
                raise _WrongData()
            tag = data[i]
            length = data[i + 1]
            value_off = i + 2
            if value_off + length > len(data):
                raise _WrongData()
            value = data[value_off : value_off + length]

            if tag == TAG_COMMAND_PARAMETERS:
                if length != 1:
                    raise _WrongData()
                out["command_parameters"] = value[0]
                seen |= 0x01
            elif tag == TAG_AUTHENTICATION_POLICY:
                if length != 1:
                    raise _WrongData()
                out["authentication_policy"] = value[0]
                seen |= 0x02
            elif tag == TAG_PROTOCOL_VERSION:
                if length != 2:
                    raise _WrongData()
                version = (value[0] << 8) | value[1]
                if version != PROTOCOL_VERSION_1_0:
                    raise _WrongData()
                seen |= 0x04
            elif tag == TAG_READER_EPUBK:
                if length != 65 or value[0] != 0x04:
                    raise _WrongData()
                out["reader_ephem_pub"] = value
                seen |= 0x08
            elif tag == TAG_TRANSACTION_ID:
                if length != 16:
                    raise _WrongData()
                out["transaction_id"] = value
                seen |= 0x10
            elif tag == TAG_READER_IDENTIFIER:
                if length != 32:
                    raise _WrongData()
                out["reader_group_id"] = value[:16]
                out["reader_group_sub_id"] = value[16:]
                seen |= 0x20
            # else: unknown tag, silently accept (spec 1.4.3)
            i = value_off + length
        if seen != 0x3F:
            raise _WrongData()
        return out

    # ---------------- AUTH1 ----------------

    def _process_auth1(self, apdu: bytes) -> tuple[bytes, int]:
        """Verify reader sig, derive keys, sign Table 8-13 with credential
        priv, build Table 8-11 plaintext, GCM-encrypt, return."""
        if not self.auth0_done:
            return b"", 0x6985  # CONDITIONS_NOT_SATISFIED
        # Mirror applet: clear AUTH0_DONE before crypto, to prevent replay.
        self.auth0_done = False

        if len(apdu) < 5:
            return b"", 0x6700
        lc = apdu[4]
        data = apdu[5 : 5 + lc]
        if len(data) != lc:
            return b"", 0x6700

        try:
            auth1 = self._parse_auth1(data)
        except _WrongData:
            return b"", 0x6A80

        cmd_params = auth1["command_parameters"]
        reader_raw_sig = auth1["reader_signature"]

        # Reconstruct Table 8-12 and verify against captured reader pub
        reader_ephem_pub = self.session.reader_ephem_pub_uncompressed
        cred_ephem_pub = self.session.credential_ephem_pub_uncompressed
        txn_id = self.session.transaction_id
        reader_id_32 = (
            self.session.reader_group_id + self.session.reader_group_sub_id
        )

        table_812 = build_table_812(
            reader_id_32=reader_id_32,
            credential_ephem_pub_x=cred_ephem_pub[1:33],
            reader_ephem_pub_x=reader_ephem_pub[1:33],
            transaction_id=txn_id,
        )
        if not self._verify_reader_sig(table_812, reader_raw_sig):
            return b"", 0x6A80

        # §8.3.1.13 flag uses the AUTH1 command_parameters (not AUTH0's).
        # Overwrite the AUTH0 value stored on the session so _derive_sk_device
        # builds salt_volatile with the right byte.
        self.session.command_parameters = cmd_params

        # Derive session keys
        sk_device = self._derive_sk_device()

        # Sign Table 8-13 with credential_priv
        table_813 = build_table_813(
            reader_id_32=reader_id_32,
            credential_ephem_pub_x=cred_ephem_pub[1:33],
            reader_ephem_pub_x=reader_ephem_pub[1:33],
            transaction_id=txn_id,
        )
        der_sig = self.credential_priv.sign(table_813, ec.ECDSA(hashes.SHA256()))
        r, s = decode_dss_signature(der_sig)
        ud_raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big")

        # Build Table 8-11 plaintext
        plaintext = self._build_table_811_plaintext(cmd_params, ud_raw_sig)

        # Encrypt with sk_device, IV = 7x00 || 01 || 0x00000001
        iv = b"\x00" * 7 + b"\x01" + (1).to_bytes(4, "big")
        ct_and_tag = AESGCM(sk_device).encrypt(iv, plaintext, associated_data=None)
        return ct_and_tag, 0x9000

    def _parse_auth1(self, data: bytes) -> dict:
        if len(data) == 0:
            raise _WrongData()
        seen = 0
        out = {}
        i = 0
        while i < len(data):
            if i + 2 > len(data):
                raise _WrongData()
            tag = data[i]
            length = data[i + 1]
            value_off = i + 2
            if value_off + length > len(data):
                raise _WrongData()
            value = data[value_off : value_off + length]

            if tag == TAG_COMMAND_PARAMETERS:
                if length != 1:
                    raise _WrongData()
                out["command_parameters"] = value[0]
                seen |= 0x01
            elif tag == TAG_READER_SIGNATURE:
                if length != 64:
                    raise _WrongData()
                out["reader_signature"] = value
                seen |= 0x02
            elif tag == TAG_READER_CERT:
                # v1: rejected explicitly
                raise _WrongData()
            # else: unknown tag, silently accept
            i = value_off + length
        if seen != 0x03:
            raise _WrongData()
        return out

    def _verify_reader_sig(self, table_812: bytes, raw_sig_64: bytes) -> bool:
        if len(raw_sig_64) != 64:
            return False
        r = int.from_bytes(raw_sig_64[:32], "big")
        s = int.from_bytes(raw_sig_64[32:], "big")
        der = encode_dss_signature(r, s)
        try:
            self.reader_pub.verify(der, table_812, ec.ECDSA(hashes.SHA256()))
            return True
        except InvalidSignature:
            return False

    def _derive_sk_device(self) -> bytes:
        """Mirrors AliroApplet.deriveSessionKeys."""
        reader_ephem_pub = self.session.reader_ephem_pub_uncompressed
        cred_ephem_pub = self.session.credential_ephem_pub_uncompressed
        txn_id = self.session.transaction_id

        # Kdh: ECDH(credential_ephem_priv, reader_ephem_pub) -> HKDF with salt=txn_id
        kdh = derive_kdh(
            self.credential_ephem_priv,
            reader_ephem_pub,
            txn_id,
        )

        # x-coord for the reader long-term key
        reader_long_term_x = self._pub_x(self.reader_pub)

        salt = build_salt_volatile(
            reader_long_term_pub_x=reader_long_term_x,
            reader_group_id=self.session.reader_group_id,
            reader_group_sub_id=self.session.reader_group_sub_id,
            reader_ephem_pub_x=reader_ephem_pub[1:33],
            transaction_id=txn_id,
            command_parameters=self.session.command_parameters,
            authentication_policy=self.session.authentication_policy,
        )

        info = cred_ephem_pub[1:33]
        derived = derive_expedited_session_keys(kdh, salt, info)
        return derived[OFF_EXPEDITED_SK_DEVICE : OFF_EXPEDITED_SK_DEVICE + 32]

    def _build_table_811_plaintext(self, cmd_params: int, ud_raw_sig: bytes) -> bytes:
        """Table 8-11 plaintext per AliroApplet.buildTable811Plaintext."""
        out = bytearray()
        if (cmd_params & 0x01) == 0:
            # 0x4E 0x08 [key_slot] = first 8B of SHA-1(uncompressed credential_PubK)
            cred_pub_uncomp = self.credential_pub.public_bytes(
                encoding=serialization.Encoding.X962,
                format=serialization.PublicFormat.UncompressedPoint,
            )
            sha1 = hashes.Hash(hashes.SHA1())
            sha1.update(cred_pub_uncomp)
            digest = sha1.finalize()
            out += bytes([0x4E, 0x08]) + digest[:8]
        else:
            # 0x5A 0x41 [0x04 || x || y]
            cred_pub_uncomp = self.credential_pub.public_bytes(
                encoding=serialization.Encoding.X962,
                format=serialization.PublicFormat.UncompressedPoint,
            )
            out += bytes([0x5A, 0x41]) + cred_pub_uncomp

        # 0x9E 0x40 [raw r||s UD signature]
        out += bytes([0x9E, 0x40]) + ud_raw_sig

        # 0x5E 0x02 [signaling_bitmap] (big-endian 16-bit)
        bitmap = 0
        if self.has_access_document:
            bitmap |= 0x0001  # bit 0: AD retrievable
            bitmap |= 0x0004  # bit 2: step-up AID required on NFC
        out += bytes([0x5E, 0x02, (bitmap >> 8) & 0xFF, bitmap & 0xFF])

        return bytes(out)

    @staticmethod
    def _pub_x(pub: ec.EllipticCurvePublicKey) -> bytes:
        nums = pub.public_numbers()
        return nums.x.to_bytes(32, "big")


class _WrongData(Exception):
    """Raised internally when TLV parsing fails; mapped to SW=6A80."""
    pass
