//! Debug/proof commands for validating NTAG 424 DNA protocol against real tags.
//!
//! These functions exist to verify transport, auth, and SDM template correctness
//! before wiring the full enrollment flow. Tag I/O functions are cfg-gated to
//! desktop platforms.

use serde::Serialize;

use crate::enrollment::{self, SdmTemplatePolicy, SdmTemplateResult};
use crate::ntag424;

// ---------------------------------------------------------------------------
// preflight_tag_state — read-only tag inspection
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize)]
pub struct PreflightResult {
    pub reader_name: String,
    pub tag_uid: String,
    pub get_version: Option<VersionSummary>,
    pub ndef_app_selected: bool,
    pub file_settings_02: Option<FileSettingsSummary>,
    pub cc_file: Option<CcSummary>,
    pub ndef_content: Option<NdefSummary>,
    pub classification: String,
    pub log: Vec<String>,
    /// SDMReadCtr from GetFileCounters(02) when auth succeeds.
    pub tag_counter: Option<u32>,
    /// "authenticated" when counter was read; "unavailable" when denied or not applicable.
    pub counter_access: Option<String>,
}

#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct VersionSummary {
    pub hw_vendor: String,
    pub hw_type: String,
    pub hw_subtype: String,
    pub hw_version: String,
    pub sw_version: String,
    pub storage_size: u8,
    pub uid: String,
    pub batch: String,
    pub is_ntag424_dna: bool,
}

#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct FileSettingsSummary {
    pub file_type: String,
    pub comm_mode: String,
    pub sdm_enabled: bool,
    pub access_rights_hex: String,
    pub file_size: u32,
    pub raw_hex: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct CcSummary {
    pub cc_len: u16,
    pub mapping_version: String,
    pub max_le: u16,
    pub max_lc: u16,
    pub ndef_fid: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct NdefSummary {
    pub ndef_len: u16,
    pub is_empty: bool,
    pub raw_prefix_hex: String,
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn resolve_counter_auth_key(
    vault_dir: Option<&std::path::Path>,
    master_password: Option<&str>,
    active_identity_id: Option<i64>,
    tag_uid: &str,
    log: &mut Vec<String>,
) -> [u8; 16] {
    let transport_key = crate::vault_keys::TRANSPORT_KEY;
    let (Some(vault_dir), Some(master_password), Some(active_identity_id)) =
        (vault_dir, master_password, active_identity_id)
    else {
        log.push("Counter auth: transport key (no vault context)".into());
        return transport_key;
    };

    let uid_normalized = tag_uid.trim().to_uppercase();
    let Ok(Some(tag)) = crate::vault::get_tag_by_uid(vault_dir, master_password, &uid_normalized) else {
        log.push("Counter auth: transport key (tag not in vault)".into());
        return transport_key;
    };

    if tag.identity_id != Some(active_identity_id) {
        log.push("Counter auth: transport key (tag not owned by active identity)".into());
        return transport_key;
    }

    let Some(kv) = tag.key_version else {
        log.push("Counter auth: transport key (no key_version)".into());
        return transport_key;
    };
    if kv <= 0 {
        log.push("Counter auth: transport key (key_version=0)".into());
        return transport_key;
    }

    let Ok(vault_secret) = crate::vault::get_vault_secret_for_tag_derivation(vault_dir, master_password) else {
        log.push("Counter auth: transport key (vault secret unavailable)".into());
        return transport_key;
    };
    let Ok(uid_bytes) = hex::decode(&uid_normalized) else {
        log.push("Counter auth: transport key (uid decode failed)".into());
        return transport_key;
    };
    if uid_bytes.len() != 7 {
        log.push("Counter auth: transport key (UID length != 7)".into());
        return transport_key;
    }

    match crate::vault_keys::derive_single_key(vault_secret.as_ref(), &uid_bytes, kv as u32, 0) {
        Ok(key) => {
            log.push(format!("Counter auth: derived key (key_version={kv})"));
            key
        }
        Err(_) => {
            log.push("Counter auth: transport key (derive failed)".into());
            transport_key
        }
    }
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn preflight_tag_state(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    vault_dir: Option<&std::path::Path>,
    master_password: Option<&str>,
    active_identity_id: Option<i64>,
) -> Result<PreflightResult, String> {
    use crate::iso7816_t4t;
    use crate::nfc;

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let reader_name = conn.reader_name().to_string();

    // --- UID via FF CA ---
    let uid_resp = conn.transmit(&[0xFF, 0xCA, 0x00, 0x00, 0x00])?;
    let tag_uid = if uid_resp.len() >= 2
        && uid_resp[uid_resp.len() - 2] == 0x90
        && uid_resp[uid_resp.len() - 1] == 0x00
    {
        let uid_bytes = &uid_resp[..uid_resp.len() - 2];
        hex::encode_upper(uid_bytes)
    } else {
        log.push(format!("UID read failed: {}", hex::encode(&uid_resp)));
        return Ok(PreflightResult {
            reader_name,
            tag_uid: String::new(),
            get_version: None,
            ndef_app_selected: false,
            file_settings_02: None,
            cc_file: None,
            ndef_content: None,
            classification: "uid_read_failed".into(),
            log,
            tag_counter: None,
            counter_access: None,
        });
    };
    log.push(format!("UID: {tag_uid}"));

    // --- GetVersion (3-frame) ---
    let get_version = run_get_version(&conn, &mut log);

    // --- SELECT NDEF application ---
    let select_resp = conn.transmit(&iso7816_t4t::cmd_select_ndef_app())?;
    let ndef_app_selected = iso7816_t4t::is_iso_success(&select_resp);
    if ndef_app_selected {
        log.push("SELECT NDEF app: OK".into());
    } else {
        log.push(format!("SELECT NDEF app: {}", hex::encode(&select_resp)));
    }

    // --- GetFileSettings(02) ---
    let file_settings_02 = if ndef_app_selected {
        run_get_file_settings(&conn, 0x02, &mut log)
    } else {
        None
    };

    // --- CC file ---
    let cc_file = run_read_cc(&conn, &mut log);

    // --- NDEF file (unauthenticated read — increments SDMReadCtr) ---
    let ndef_content = run_read_ndef(&conn, &mut log);

    // --- GetFileCounters(02) when SDM enabled: auth + GetFileCounters (SDMCtrRet=0 requires auth)
    // Run AFTER run_read_ndef so displayed counter includes our scan's increment (+1 per scan).
    let (tag_counter, counter_access) = if ndef_app_selected
        && file_settings_02.as_ref().map_or(false, |fs| fs.sdm_enabled)
    {
        let auth_key = resolve_counter_auth_key(vault_dir, master_password, active_identity_id, &tag_uid, &mut log);
        run_get_file_counters_after_auth(&mut conn, &auth_key, &mut log)
    } else {
        (None, Some("unavailable".into()))
    };

    // --- Classification ---
    let classification = classify_tag(&get_version, &file_settings_02, &ndef_content);
    log.push(format!("classification: {classification}"));

    Ok(PreflightResult {
        reader_name,
        tag_uid,
        get_version,
        ndef_app_selected,
        file_settings_02,
        cc_file,
        ndef_content,
        classification,
        log,
        tag_counter,
        counter_access,
    })
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn run_get_version(
    conn: &crate::nfc::NfcConnection,
    log: &mut Vec<String>,
) -> Option<VersionSummary> {
    let r1 = match conn.transmit(&ntag424::cmd_get_version()) {
        Ok(r) => r,
        Err(e) => { log.push(format!("GetVersion failed: {e}")); return None; }
    };
    let d1 = match ntag424::expect_additional_frame(&r1) {
        Ok(d) => d,
        Err(e) => { log.push(format!("GetVersion frame1: {e}")); return None; }
    };

    let r2 = match conn.transmit(&ntag424::cmd_additional_frame()) {
        Ok(r) => r,
        Err(e) => { log.push(format!("GetVersion AF#1 failed: {e}")); return None; }
    };
    let d2 = match ntag424::expect_additional_frame(&r2) {
        Ok(d) => d,
        Err(e) => { log.push(format!("GetVersion frame2: {e}")); return None; }
    };

    let r3 = match conn.transmit(&ntag424::cmd_additional_frame()) {
        Ok(r) => r,
        Err(e) => { log.push(format!("GetVersion AF#2 failed: {e}")); return None; }
    };
    let d3 = match ntag424::expect_success(&r3) {
        Ok(d) => d,
        Err(e) => { log.push(format!("GetVersion frame3: {e}")); return None; }
    };

    let mut combined = Vec::with_capacity(d1.len() + d2.len() + d3.len());
    combined.extend_from_slice(&d1);
    combined.extend_from_slice(&d2);
    combined.extend_from_slice(&d3);

    match ntag424::parse_get_version(&combined) {
        Ok(v) => {
            let summary = VersionSummary {
                hw_vendor: format!("{:02X}", v.hw_vendor),
                hw_type: format!("{:02X}", v.hw_type),
                hw_subtype: format!("{:02X}", v.hw_subtype),
                hw_version: format!("{}.{}", v.hw_major, v.hw_minor),
                sw_version: format!("{}.{}", v.sw_major, v.sw_minor),
                storage_size: v.hw_storage_size,
                uid: v.uid_hex(),
                batch: hex::encode(v.batch_no),
                is_ntag424_dna: v.is_ntag424_dna(),
            };
            log.push(format!(
                "GetVersion: vendor={} type={} sub={} ntag424={}",
                summary.hw_vendor, summary.hw_type, summary.hw_subtype, summary.is_ntag424_dna
            ));
            Some(summary)
        }
        Err(e) => {
            log.push(format!("GetVersion parse error: {e}"));
            None
        }
    }
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn run_get_file_settings(
    conn: &crate::nfc::NfcConnection,
    file_no: u8,
    log: &mut Vec<String>,
) -> Option<FileSettingsSummary> {
    let resp = match conn.transmit(&ntag424::cmd_get_file_settings(file_no)) {
        Ok(r) => r,
        Err(e) => { log.push(format!("GetFileSettings({file_no:02X}) transmit: {e}")); return None; }
    };
    let data = match ntag424::expect_success(&resp) {
        Ok(d) => d,
        Err(e) => { log.push(format!("GetFileSettings({file_no:02X}): {e}")); return None; }
    };
    let raw_hex = hex::encode(&data);
    match ntag424::parse_file_settings(&data) {
        Ok(fs) => {
            let cm = match fs.comm_mode {
                ntag424::CommMode::Plain => "Plain",
                ntag424::CommMode::Mac => "MAC",
                ntag424::CommMode::Full => "Full",
            };
            let summary = FileSettingsSummary {
                file_type: format!("{:02X}", fs.file_type),
                comm_mode: cm.to_string(),
                sdm_enabled: fs.sdm_enabled,
                access_rights_hex: format!("{:04X}", fs.access_rights),
                file_size: fs.file_size,
                raw_hex,
            };
            log.push(format!(
                "FileSettings(02): comm={} sdm={} size={}",
                cm, fs.sdm_enabled, fs.file_size
            ));
            Some(summary)
        }
        Err(e) => {
            log.push(format!("FileSettings parse: {e}"));
            None
        }
    }
}

/// Auth with key 0 (admin path) and call GetFileCounters(02). Returns (counter, access_status).
/// SDMCtrRet=0 requires auth; uses transport key for tags that have not rotated keys.
/// GetFileCounters must be sent as authenticated (MACed) command when access requires auth.
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn run_get_file_counters_after_auth(
    conn: &mut crate::nfc::NfcConnection,
    auth_key: &[u8; 16],
    log: &mut Vec<String>,
) -> (Option<u32>, Option<String>) {
    use crate::ev2_auth;
    use crate::ntag424;

    let result = conn.with_transaction(|transmit| {
        // Auth EV2 First with key 0
        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, auth_key, 0x00)
            .map_err(|e| format!("Auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Auth complete: {e}"))?;

        // GetFileCounters(02) — must be sent as authenticated (MACed) command when SDMCtrRet requires auth
        let f6_apdu = session.build_maced_command(0xF6, &[0x02], &[], true);
        let f6_resp = transmit(&f6_apdu)?;
        let resp = ntag424::parse_nxp_response(&f6_resp)
            .ok_or_else(|| format!("GetFileCounters: not NXP response"))?;
        if !resp.status.is_success() {
            return Err(format!("GetFileCounters: {}", resp.status));
        }
        // Response format: plain [Ctr3], or [Encrypted16,MAC8] (CommMode.Full), or [Ctr3,MAC8] (CommMode.MAC).
        let data: Vec<u8> = if resp.data.len() == 3 {
            resp.data.to_vec()
        } else if resp.data.len() == 24 {
            // 16 bytes encrypted + 8 bytes MAC. Verify MAC, then decrypt.
            let encrypted = session
                .verify_response_mac_with_ctr(0x00, &resp.data, Some(1))
                .or_else(|_| session.verify_response_mac_with_ctr(0x00, &resp.data, Some(0)))
                .map_err(|e| format!("MAC verify: {e}"))?;
            if encrypted.len() != 16 {
                return Err(format!("GetFileCounters: expected 16-byte encrypted payload, got {}", encrypted.len()));
            }
            session.decrypt_response(&encrypted).map_err(|e| format!("decrypt: {e}"))?
        } else if resp.data.len() == 11 {
            // 3 bytes + 8 MAC (CommMode.MAC, plain response)
            session.verify_response_mac_with_ctr(0x00, &resp.data, Some(1))
                .or_else(|_| session.verify_response_mac_with_ctr(0x00, &resp.data, Some(0)))
                .unwrap_or_else(|_| resp.data[..3].to_vec())
        } else {
            return Err(format!("GetFileCounters: unexpected response length {}", resp.data.len()));
        };
        if data.len() >= 3 {
            let ctr = (u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16))
                & 0xFF_FF_FF;
            log.push(format!("GetFileCounters(02): {ctr} (authenticated)"));
            Ok(ctr)
        } else {
            Err("GetFileCounters: payload too short".into())
        }
    });

    match result {
        Ok(ctr) => {
            (Some(ctr), Some("authenticated".into()))
        }
        Err(e) => {
            log.push(format!("GetFileCounters: {e}"));
            (None, Some(format!("unavailable ({e})")))
        }
    }
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn run_read_cc(
    conn: &crate::nfc::NfcConnection,
    log: &mut Vec<String>,
) -> Option<CcSummary> {
    use crate::iso7816_t4t;

    let sel = conn.transmit(&iso7816_t4t::cmd_select_cc_file()).ok()?;
    if !iso7816_t4t::is_iso_success(&sel) {
        log.push(format!("SELECT CC: {}", hex::encode(&sel)));
        return None;
    }
    let read_resp = conn.transmit(&iso7816_t4t::cmd_read_binary(0, 32)).ok()?;
    let data = match iso7816_t4t::extract_iso_payload(&read_resp) {
        Ok(d) => d,
        Err(e) => { log.push(format!("ReadBinary CC: {e}")); return None; }
    };
    match iso7816_t4t::parse_cc_file(&data) {
        Ok(cc) => {
            let ndef_fid = cc.ndef_file_control.as_ref().map(|n| format!("{:04X}", n.file_id));
            log.push(format!(
                "CC: len={} v={:02X} MLe={} MLc={} ndef_fid={:?}",
                cc.cc_len, cc.mapping_version, cc.max_le, cc.max_lc, ndef_fid
            ));
            Some(CcSummary {
                cc_len: cc.cc_len,
                mapping_version: format!("{:02X}", cc.mapping_version),
                max_le: cc.max_le,
                max_lc: cc.max_lc,
                ndef_fid,
            })
        }
        Err(e) => { log.push(format!("CC parse: {e}")); None }
    }
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn run_read_ndef(
    conn: &crate::nfc::NfcConnection,
    log: &mut Vec<String>,
) -> Option<NdefSummary> {
    use crate::iso7816_t4t;

    let sel = conn.transmit(&iso7816_t4t::cmd_select_ndef_file()).ok()?;
    if !iso7816_t4t::is_iso_success(&sel) {
        log.push(format!("SELECT NDEF: {}", hex::encode(&sel)));
        return None;
    }
    // Read first pass with Le=0 (up to 256 bytes)
    let read_resp = conn.transmit(&iso7816_t4t::cmd_read_binary(0, 0)).ok()?;
    let data = match iso7816_t4t::extract_iso_payload(&read_resp) {
        Ok(d) => d,
        Err(e) => { log.push(format!("ReadBinary NDEF: {e}")); return None; }
    };

    if data.len() < 2 {
        log.push("NDEF file too short".into());
        return None;
    }
    let ndef_len = u16::from_be_bytes([data[0], data[1]]);
    let is_empty = ndef_len == 0;
    let prefix_bytes = std::cmp::min(data.len(), 32);
    let raw_prefix_hex = hex::encode(&data[..prefix_bytes]);
    log.push(format!("NDEF: nlen={ndef_len} empty={is_empty}"));

    Some(NdefSummary {
        ndef_len,
        is_empty,
        raw_prefix_hex,
    })
}

// ---------------------------------------------------------------------------
// read_back_enrollment_state — GetFileSettings(02), NDEF bytes, classification
// ---------------------------------------------------------------------------

/// Result of reading back enrollment state from a live tag.
/// Used for baseline comparison and regression testing.
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct EnrollmentStateReadback {
    pub reader_name: String,
    pub tag_uid: String,
    pub file_settings_02_raw_hex: String,
    pub file_settings_02: Option<FileSettingsSummary>,
    pub ndef_bytes_hex: String,
    pub ndef_len: u16,
    pub classification: String,
    pub get_version: Option<VersionSummary>,
    /// SDMReadCtr from GetFileCounters(02) when auth succeeds.
    pub tag_counter: Option<u32>,
    /// "authenticated" when counter was read; "unavailable" when denied or not applicable.
    pub counter_access: Option<String>,
}

/// Persisted baseline: enrollment state plus save timestamp.
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct PersistedEnrollmentBaseline {
    pub readback: EnrollmentStateReadback,
    pub saved_at: String,
}

/// Read full NDEF file content using native ReadData(02).
/// Per docs/nfc.md: after WriteData use ReadData — not ISO ReadBinary (which would require SELECT).
/// NDEF app must already be selected (e.g. by read_back_enrollment_state).
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn run_read_ndef_full(
    conn: &crate::nfc::NfcConnection,
    log: &mut Vec<String>,
) -> Option<(u16, Vec<u8>)> {
    use crate::ntag424;

    // ReadData(02, 0, 256) — native command, no SELECT file needed
    let read_apdu = ntag424::cmd_read_data_plain(0x02, 0, 256);
    let read_resp = conn.transmit(&read_apdu).ok()?;
    let data = match ntag424::expect_success(&read_resp) {
        Ok(d) => d,
        Err(e) => {
            log.push(format!("ReadData(02) NDEF: {e}"));
            return None;
        }
    };

    if data.len() < 2 {
        log.push("NDEF file too short".into());
        return None;
    }
    let ndef_len = u16::from_be_bytes([data[0], data[1]]);
    log.push(format!("NDEF: nlen={ndef_len} len={}", data.len()));

    Some((ndef_len, data))
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn read_back_enrollment_state(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    vault_dir: Option<&std::path::Path>,
    master_password: Option<&str>,
    active_identity_id: Option<i64>,
) -> Result<EnrollmentStateReadback, String> {
    use crate::iso7816_t4t;
    use crate::nfc;

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let reader_name = conn.reader_name().to_string();

    // UID via FF CA
    let uid_resp = conn.transmit(&[0xFF, 0xCA, 0x00, 0x00, 0x00])?;
    let tag_uid = if uid_resp.len() >= 2
        && uid_resp[uid_resp.len() - 2] == 0x90
        && uid_resp[uid_resp.len() - 1] == 0x00
    {
        hex::encode_upper(&uid_resp[..uid_resp.len() - 2])
    } else {
        return Err(format!("UID read failed: {}", hex::encode(&uid_resp)));
    };

    // GetVersion
    let get_version = run_get_version(&conn, &mut log);

    // SELECT NDEF application
    let select_resp = conn.transmit(&iso7816_t4t::cmd_select_ndef_app())?;
    let ndef_app_selected = iso7816_t4t::is_iso_success(&select_resp);
    if !ndef_app_selected {
        return Err(format!(
            "SELECT NDEF app failed: {}",
            hex::encode(&select_resp)
        ));
    }

    // GetFileSettings(02) — raw and parsed
    let (file_settings_02_raw_hex, file_settings_02) = match conn.transmit(&ntag424::cmd_get_file_settings(0x02)) {
        Ok(resp) => match ntag424::expect_success(&resp) {
            Ok(data) => {
                let raw_hex = hex::encode(&data);
                let parsed = ntag424::parse_file_settings(&data).ok().map(|fs| {
                    let cm = match fs.comm_mode {
                        ntag424::CommMode::Plain => "Plain",
                        ntag424::CommMode::Mac => "MAC",
                        ntag424::CommMode::Full => "Full",
                    };
                    FileSettingsSummary {
                        file_type: format!("{:02X}", fs.file_type),
                        comm_mode: cm.to_string(),
                        sdm_enabled: fs.sdm_enabled,
                        access_rights_hex: format!("{:04X}", fs.access_rights),
                        file_size: fs.file_size,
                        raw_hex: raw_hex.clone(),
                    }
                });
                (raw_hex, parsed)
            }
            Err(e) => return Err(format!("GetFileSettings(02): {e}")),
        },
        Err(e) => return Err(format!("GetFileSettings(02) transmit: {e}")),
    };

    // GetFileCounters(02) when SDM enabled: auth + GetFileCounters (SDMCtrRet=0 requires auth)
    let (tag_counter, counter_access) = if file_settings_02.as_ref().map_or(false, |fs| fs.sdm_enabled) {
        let auth_key = resolve_counter_auth_key(vault_dir, master_password, active_identity_id, &tag_uid, &mut log);
        run_get_file_counters_after_auth(&mut conn, &auth_key, &mut log)
    } else {
        (None, Some("unavailable".into()))
    };

    // Re-select NDEF app (transaction may have affected state), then ReadData(02)
    let sel_resp = conn.transmit(&iso7816_t4t::cmd_select_ndef_app())?;
    if !iso7816_t4t::is_iso_success(&sel_resp) {
        return Err(format!(
            "SELECT NDEF app failed before read: {}",
            hex::encode(&sel_resp)
        ));
    }
    let (ndef_len, ndef_bytes_hex) = match run_read_ndef_full(&conn, &mut log) {
        Some((len, bytes)) => (len, hex::encode(&bytes)),
        None => return Err("NDEF read failed".into()),
    };

    // Classification
    let ndef_summary = Some(NdefSummary {
        ndef_len,
        is_empty: ndef_len == 0,
        raw_prefix_hex: ndef_bytes_hex.chars().take(64).collect::<String>(),
    });
    let classification = classify_tag(&get_version, &file_settings_02, &ndef_summary);

    Ok(EnrollmentStateReadback {
        reader_name,
        tag_uid,
        file_settings_02_raw_hex,
        file_settings_02,
        ndef_bytes_hex,
        ndef_len,
        classification,
        get_version,
        tag_counter,
        counter_access,
    })
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn read_back_enrollment_state(
    _reader: Option<&str>,
    _timeout: u64,
    _vault_dir: Option<&std::path::Path>,
    _master_password: Option<&str>,
    _active_identity_id: Option<i64>,
) -> Result<EnrollmentStateReadback, String> {
    Err("NFC not supported on this platform".into())
}

fn classify_tag(
    version: &Option<VersionSummary>,
    fs: &Option<FileSettingsSummary>,
    ndef: &Option<NdefSummary>,
) -> String {
    let is_424 = version.as_ref().map_or(false, |v| v.is_ntag424_dna);
    let sdm_on = fs.as_ref().map_or(false, |f| f.sdm_enabled);
    let ndef_empty = ndef.as_ref().map_or(true, |n| n.is_empty);
    let plain_comm = fs.as_ref().map_or(false, |f| f.comm_mode == "Plain");

    if !is_424 {
        return "not_ntag424_dna".into();
    }
    if sdm_on {
        return "sdm_enabled".into();
    }
    if plain_comm && ndef_empty {
        return "factory_default_likely".into();
    }
    if plain_comm && !ndef_empty {
        return "plain_ndef".into();
    }
    "custom_keys_unknown".into()
}

// ---------------------------------------------------------------------------
// debug_auth_ev2_first — attempt auth with zero key
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize)]
pub struct AuthDebugResult {
    pub part1_success: bool,
    pub encrypted_rndb_hex: Option<String>,
    pub part2_success: bool,
    pub ti_hex: Option<String>,
    pub session_key_fingerprint: Option<String>,
    pub final_status: String,
    pub log: Vec<String>,
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn debug_auth_ev2_first(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
) -> Result<AuthDebugResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;
    use sha2::{Sha256, Digest};

    let conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let zero_key = [0u8; 16];

    // SELECT NDEF application first (required for DESFire-wrapped commands)
    let sel = conn.transmit(&iso7816_t4t::cmd_select_ndef_app())?;
    if !iso7816_t4t::is_iso_success(&sel) {
        return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel)));
    }
    log.push("SELECT NDEF app: OK".into());

    // Auth part 1: send 71h with key 00
    let part1_apdu = ntag424::cmd_auth_ev2_first_part1(0x00);
    log.push(format!("Auth part1 APDU: {}", hex::encode(&part1_apdu)));
    let part1_resp = conn.transmit(&part1_apdu)?;
    log.push(format!("Auth part1 resp: {}", hex::encode(&part1_resp)));

    let part1_data = match ntag424::expect_additional_frame(&part1_resp) {
        Ok(d) => d,
        Err(e) => {
            return Ok(AuthDebugResult {
                part1_success: false,
                encrypted_rndb_hex: None,
                part2_success: false,
                ti_hex: None,
                session_key_fingerprint: None,
                final_status: format!("part1 failed: {e}"),
                log,
            });
        }
    };
    let encrypted_rndb_hex = hex::encode(&part1_data);
    log.push(format!("Encrypted RndB: {encrypted_rndb_hex}"));

    // Process part 1 and prepare part 2
    let challenge = match ev2_auth::process_auth_part1(&part1_data, &zero_key, 0x00) {
        Ok(c) => c,
        Err(e) => {
            return Ok(AuthDebugResult {
                part1_success: true,
                encrypted_rndb_hex: Some(encrypted_rndb_hex),
                part2_success: false,
                ti_hex: None,
                session_key_fingerprint: None,
                final_status: format!("crypto processing failed: {e}"),
                log,
            });
        }
    };

    // Auth part 2: send AFh with encrypted challenge response
    let part2_apdu = ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload);
    log.push(format!("Auth part2 APDU: {} bytes", part2_apdu.len()));
    let part2_resp = conn.transmit(&part2_apdu)?;
    log.push(format!("Auth part2 resp: {}", hex::encode(&part2_resp)));

    let part2_data = match ntag424::expect_success(&part2_resp) {
        Ok(d) => d,
        Err(e) => {
            return Ok(AuthDebugResult {
                part1_success: true,
                encrypted_rndb_hex: Some(encrypted_rndb_hex),
                part2_success: false,
                ti_hex: None,
                session_key_fingerprint: None,
                final_status: format!("part2 failed: {e}"),
                log,
            });
        }
    };

    // Complete auth
    match ev2_auth::complete_auth(challenge, &part2_data) {
        Ok(session) => {
            let ti_hex = hex::encode(session.ti);
            // Session key fingerprint: SHA-256 of (ks_enc || ks_mac), first 8 bytes hex
            let mut hasher = Sha256::new();
            hasher.update(session.ks_enc);
            hasher.update(session.ks_mac);
            let hash = hasher.finalize();
            let fingerprint = hex::encode(&hash[..8]);

            log.push(format!("TI: {ti_hex}"));
            log.push(format!("Session key fingerprint: {fingerprint}"));
            log.push("Auth complete: SUCCESS".into());

            Ok(AuthDebugResult {
                part1_success: true,
                encrypted_rndb_hex: Some(encrypted_rndb_hex),
                part2_success: true,
                ti_hex: Some(ti_hex),
                session_key_fingerprint: Some(fingerprint),
                final_status: "success".into(),
                log,
            })
        }
        Err(e) => {
            Ok(AuthDebugResult {
                part1_success: true,
                encrypted_rndb_hex: Some(encrypted_rndb_hex),
                part2_success: false,
                ti_hex: None,
                session_key_fingerprint: None,
                final_status: format!("auth completion failed: {e}"),
                log,
            })
        }
    }
}

// ---------------------------------------------------------------------------
// debug_build_sdm_template — pure local, no I/O
// ---------------------------------------------------------------------------

/// Wrapper that delegates to `enrollment::build_sdm_template` and returns
/// the full result for the Tauri command.
pub fn debug_build_sdm_template(
    base_url: &str,
    policy: &SdmTemplatePolicy,
) -> SdmTemplateResult {
    enrollment::build_sdm_template(base_url, policy, None, None, None, None)
}

// ---------------------------------------------------------------------------
// debug_change_file_settings_dry_run — no transmit
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize)]
pub struct DryRunChangeFileSettingsResult {
    /// Human-readable description of the intended settings.
    pub parsed_settings: Vec<String>,
    /// Raw ChangeFileSettings plaintext payload (hex).
    pub plaintext_hex: String,
    /// Byte length of the plaintext payload.
    pub plaintext_len: usize,
    /// The NXP wrapped APDU that would be sent (in plain/unauthenticated mode).
    /// In authenticated mode, the data section would be encrypted + MACed.
    pub apdu_hex: String,
    /// Warnings or notes about the configuration.
    pub warnings: Vec<String>,
}

/// Dry run using the exact NXP AN12196 example (known-good). Use for regression and enrollment.
pub fn dry_run_change_file_settings_known_good() -> DryRunChangeFileSettingsResult {
    let plaintext = ntag424::nxp_example_cmd_data();
    let plaintext_hex = hex::encode(&plaintext);
    let plaintext_len = plaintext.len();
    let apdu = ntag424::cmd_change_file_settings(0x02, &plaintext);
    let apdu_hex = hex::encode(&apdu);

    let parsed = vec![
        "target: file 02 (NDEF)".into(),
        "comm_mode: Plain".into(),
        "access_rights: 00E0 (read_write=key0, change=key0, read=free, write=key0)".into(),
        "sdm_options: C1 (UID+ReadCtr mirror, encrypted PICC)".into(),
        "sdm_meta_read_key: 02".into(),
        "sdm_file_read_key: 01".into(),
        "sdm_ctr_ret_key: 01 (Model A)".into(),
        "picc_data_offset: 32 (0x000020)".into(),
        "mac_offset: 67 (0x000043)".into(),
        "mac_input_offset: 67 (0x000043)".into(),
    ];

    DryRunChangeFileSettingsResult {
        parsed_settings: parsed,
        plaintext_hex,
        plaintext_len,
        apdu_hex,
        warnings: vec![],
    }
}

/// Dry run using custom template (charm.farm). Experimental — for gradual policy reintroduction.
pub fn dry_run_change_file_settings(
    base_url: &str,
    policy: &SdmTemplatePolicy,
    meta_read_key_no: u8,
    file_read_key_no: u8,
) -> DryRunChangeFileSettingsResult {
    let template = enrollment::build_sdm_template(base_url, policy, None, None, None, None);
    let sdm_cfg = enrollment::build_enrollment_sdm_config(
        &template,
        meta_read_key_no,
        file_read_key_no,
        None,
        false,
        false,
    );

    let plaintext = ntag424::build_sdm_file_settings(&sdm_cfg);
    let plaintext_hex = hex::encode(&plaintext);
    let plaintext_len = plaintext.len();

    // Build the APDU that would be sent (plaintext version for inspection)
    let apdu = ntag424::cmd_change_file_settings(0x02, &plaintext);
    let apdu_hex = hex::encode(&apdu);

    let mut parsed = Vec::new();
    parsed.push(format!("target: file 02 (NDEF)"));
    parsed.push(format!("comm_mode: {:?}", sdm_cfg.comm_mode));
    parsed.push(format!("access_rights: {:04X} ({})", sdm_cfg.access_rights, ntag424::decode_access_rights(sdm_cfg.access_rights).join(", ")));
    let picc_mode = if sdm_cfg.sdm_meta_read_key <= 0x04 { "encrypted PICC" } else { "plaintext UID+ReadCtr" };
    parsed.push(format!("sdm_options: {:02X} (UID+ReadCtr mirror, ASCII, {})", sdm_cfg.sdm_options, picc_mode));
    parsed.push(format!("sdm_meta_read_key: {:02X}", sdm_cfg.sdm_meta_read_key));
    parsed.push(format!("sdm_file_read_key: {:02X}", sdm_cfg.sdm_file_read_key));
    parsed.push(format!("sdm_ctr_ret_key: {:02X} (auth with key {} required for GetFileCounters)", sdm_cfg.sdm_ctr_ret_key, sdm_cfg.sdm_ctr_ret_key));
    if let Some(off) = sdm_cfg.picc_data_offset {
        parsed.push(format!("picc_data_offset: {off} (0x{off:06X})"));
    }
    if let Some(off) = sdm_cfg.uid_offset {
        parsed.push(format!("uid_offset: {off} (0x{off:06X})"));
    }
    if let Some(off) = sdm_cfg.read_ctr_offset {
        parsed.push(format!("read_ctr_offset: {off} (0x{off:06X})"));
    }
    if let Some(off) = sdm_cfg.mac_input_offset {
        parsed.push(format!("mac_input_offset: {off} (0x{off:06X})"));
    }
    if let Some(off) = sdm_cfg.mac_offset {
        parsed.push(format!("mac_offset: {off} (0x{off:06X})"));
    }

    let mut warnings = Vec::new();
    if template.ndef_file_len > 256 {
        warnings.push(format!(
            "NDEF template is {} bytes — exceeds NTAG 424 DNA 256-byte file",
            template.ndef_file_len
        ));
    }
    if plaintext_len % 16 != 0 {
        warnings.push(format!(
            "plaintext is {} bytes — not block-aligned (padding needed for encrypted mode)",
            plaintext_len
        ));
    }
    // Verify offset ordering sanity (mac_offset > mac_input_offset for positive MAC input range)
    let mac_off = sdm_cfg.mac_offset.unwrap_or(0);
    let mac_in = sdm_cfg.mac_input_offset.unwrap_or(0);
    if mac_off < mac_in {
        warnings.push("mac_offset < mac_input_offset — CMAC would cover negative range".into());
    }
    if let Some(picc) = sdm_cfg.picc_data_offset {
        if picc >= mac_off {
            warnings.push("picc_data_offset >= mac_offset — PICCData is not covered by CMAC".into());
        }
    }

    DryRunChangeFileSettingsResult {
        parsed_settings: parsed,
        plaintext_hex,
        plaintext_len,
        apdu_hex,
        warnings,
    }
}

// ---------------------------------------------------------------------------
// debug_change_file_settings_execute — real ChangeFileSettings on tag
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize)]
pub struct ChangeFileSettingsExecuteResult {
    pub success: bool,
    pub pre_transmit: Vec<String>,
    pub apdu_hex: String,
    pub response_hex: String,
    pub response_status: String,
    pub post_get_file_settings: Option<String>,
    pub post_ndef_hex: Option<String>,
    pub log: Vec<String>,
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn debug_change_file_settings_execute(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    base_url: &str,
    use_nxp_example: bool,
    use_nxp_compat_for_custom: bool,
    use_nxp_offsets_for_custom: bool,
    try_cmd_ctr_zero: bool,
) -> Result<ChangeFileSettingsExecuteResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let zero_key = [0u8; 16];
    let policy = SdmTemplatePolicy::default();

    conn.with_transaction(|transmit| {
        // SELECT NDEF app
        let sel = transmit(&iso7816_t4t::cmd_select_ndef_app())?;
        if !iso7816_t4t::is_iso_success(&sel) {
            return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel)));
        }
        log.push("SELECT NDEF app: OK".into());

        // Auth EV2 First
        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, &zero_key, 0x00)
            .map_err(|e| format!("Auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Auth complete: {e}"))?;
        log.push("Auth EV2: OK".into());

        // Build plaintext: NXP example for serializer verification, or custom config
        let plaintext = if use_nxp_example {
            ntag424::nxp_example_cmd_data()
        } else {
            let template = enrollment::build_sdm_template(base_url, &policy, None, None, None, None);
            let sdm_cfg = enrollment::build_enrollment_sdm_config(
                &template,
                0x01,
                0x02,
                None,
                use_nxp_compat_for_custom,
                use_nxp_offsets_for_custom,
            );
            ntag424::build_sdm_file_settings(&sdm_cfg)
        };

        // Pre-transmit summary
        let mut pre_transmit = Vec::new();
        if use_nxp_example {
            pre_transmit.push("plaintext: NXP AN12196 example (4000E0C1F121200000430000430000)".into());
        } else {
            let template = enrollment::build_sdm_template(base_url, &policy, None, None, None, None);
            let sdm_cfg = enrollment::build_enrollment_sdm_config(
                &template,
                0x01,
                0x02,
                None,
                use_nxp_compat_for_custom,
                use_nxp_offsets_for_custom,
            );
            pre_transmit.push(format!("access_rights: {:04X} ({})", sdm_cfg.access_rights, ntag424::decode_access_rights(sdm_cfg.access_rights).join(", ")));
            pre_transmit.push(format!("sdm_options: {:02X}", sdm_cfg.sdm_options));
            if let Some(off) = sdm_cfg.picc_data_offset {
                pre_transmit.push(format!("picc_data_offset: {off}"));
            }
            pre_transmit.push(format!("mac_input_offset: {}", sdm_cfg.mac_input_offset.unwrap_or(0)));
            pre_transmit.push(format!("mac_offset: {}", sdm_cfg.mac_offset.unwrap_or(0)));
        }
        pre_transmit.push(format!("plaintext_len: {} (padded to block size)", plaintext.len()));
        if try_cmd_ctr_zero {
            pre_transmit.push("cmd_ctr: 0 (first command)".into());
        }

        let padded_len = ((plaintext.len() + 1 + 15) / 16) * 16;
        pre_transmit.push(format!("padded_plaintext_len: {padded_len}"));
        pre_transmit.push(format!("encrypted_len: {padded_len}"));
        pre_transmit.push("mac_len: 8".into());

        // Build authenticated APDU (ChangeFileSettings uses compact format: CmdHeader || Encrypted || MAC)
        let apdu = session.build_change_file_settings_command(&[0x02], &plaintext, try_cmd_ctr_zero);
        let apdu_hex = hex::encode(&apdu);
        pre_transmit.push(format!("apdu_len: {}", apdu.len()));
        pre_transmit.push(format!("apdu_hex: {}...", &apdu_hex[..apdu_hex.len().min(80)]));

        // Transmit
        log.push("Transmitting ChangeFileSettings...".into());
        let response = transmit(&apdu)?;
        let response_hex = hex::encode(&response);

        let (success, response_status) = if response.len() >= 2 {
            let sw1 = response[response.len() - 2];
            let sw2 = response[response.len() - 1];
            if sw1 == 0x91 && sw2 == 0x00 {
                (true, "9100 success".into())
            } else {
                (false, format!("{sw1:02X}{sw2:02X}"))
            }
        } else {
            (false, "invalid response".into())
        };
        log.push(format!("Response: {response_status}"));

        // Post-transmit: GetFileSettings(02), SELECT E104, ReadBinary
        let mut post_fs = None;
        let mut post_ndef = None;
        if success {
            let fs_resp = transmit(&ntag424::cmd_get_file_settings(0x02))?;
            if let Ok(data) = ntag424::expect_success(&fs_resp) {
                post_fs = Some(hex::encode(data));
            }
            let _ = transmit(&iso7816_t4t::cmd_select_ndef_file())?;
            let read_resp = transmit(&iso7816_t4t::cmd_read_binary(0, 0))?;
            if let Ok(data) = iso7816_t4t::extract_iso_payload(&read_resp) {
                post_ndef = Some(hex::encode(&data));
            }
        }

        Ok(ChangeFileSettingsExecuteResult {
            success,
            pre_transmit,
            apdu_hex,
            response_hex,
            response_status,
            post_get_file_settings: post_fs,
            post_ndef_hex: post_ndef,
            log,
        })
    })
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn debug_change_file_settings_execute(
    _reader: Option<&str>,
    _timeout: u64,
    _base_url: &str,
    _use_nxp_example: bool,
    _use_nxp_compat_for_custom: bool,
    _use_nxp_offsets_for_custom: bool,
    _try_cmd_ctr_zero: bool,
) -> Result<ChangeFileSettingsExecuteResult, String> {
    Err("NFC not supported on this platform".into())
}

// ---------------------------------------------------------------------------
// rewrite_tag_ndef_execute — rewrite NDEF using vault-derived key when tag is enrolled
// ---------------------------------------------------------------------------

/// Rewrite NDEF on a tag. Uses vault-derived key when the tag has key_version (enrolled).
/// Falls back to transport key for plain/factory tags.
/// Only the enrolling identity can rewrite; released tags (identity_id = None) can be taken by active_identity_id.
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn rewrite_tag_ndef_execute(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    base_url: &str,
    vault_dir: &std::path::Path,
    master_password: &str,
    active_identity_id: i64,
) -> Result<WriteNdefExecuteResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;
    use crate::ntag424;

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let policy = SdmTemplatePolicy::default();
    let transport_key = crate::vault_keys::TRANSPORT_KEY;

    conn.with_transaction(|transmit| {
        // 0. Read UID first (before auth)
        let tag_uid = {
            let uid_resp = transmit(&[0xFF, 0xCA, 0x00, 0x00, 0x00])?;
            if uid_resp.len() >= 2
                && uid_resp[uid_resp.len() - 2] == 0x90
                && uid_resp[uid_resp.len() - 1] == 0x00
            {
                let uid_bytes = &uid_resp[..uid_resp.len() - 2];
                Some(hex::encode_upper(uid_bytes))
            } else {
                None
            }
        };

        // Fetch artwork art_id (IPFS CID), instance_id (C2PA), claim_hash (ch), phash when tag is linked
        let (art_id_opt, instance_id_opt, ch_opt, phash_opt) = if let Some(ref uid_str) = tag_uid {
            use sha2::{Digest, Sha256};
            let uid_normalized = uid_str.trim().to_uppercase();
            if let Ok(Some(tag)) = crate::vault::get_tag_by_uid(vault_dir, master_password, &uid_normalized) {
                if let Some(artwork_id) = tag.artwork_id {
                    if let Ok(Some(draft)) = crate::vault::get_draft_by_id(vault_dir, master_password, artwork_id) {
                        let c = draft.ipfs_cid.as_ref()
                            .map(|s| s.trim())
                            .filter(|s| !s.is_empty())
                            .map(|s| s.to_string());
                        let m = draft.c2pa_manifest_id.as_ref()
                            .map(|s| s.trim())
                            .filter(|s| !s.is_empty())
                            .map(|s| s.to_string());
                        let ch = draft.c2pa_claim_id.as_ref()
                            .map(|s| s.trim())
                            .filter(|s| !s.is_empty() && s.starts_with("urn:"))
                            .map(|s| {
                                let mut hasher = Sha256::new();
                                hasher.update(s.as_bytes());
                                let digest = hasher.finalize();
                                hex::encode(&digest[..8])
                            });
                        let ph = draft.perceptual_hash.as_ref()
                            .map(|s| s.trim())
                            .filter(|s| !s.is_empty())
                            .map(|s| s.to_string());
                        if c.is_some() || m.is_some() || ch.is_some() || ph.is_some() {
                            log.push("Including artwork art_id, instance_id, ch, and phash in NDEF".into());
                        }
                        (c, m, ch, ph)
                    } else {
                        (None, None, None, None)
                    }
                } else {
                    (None, None, None, None)
                }
            } else {
                (None, None, None, None)
            }
        } else {
            (None, None, None, None)
        };

        let template = enrollment::build_sdm_template(base_url, &policy, art_id_opt.as_deref(), instance_id_opt.as_deref(), ch_opt.as_deref(), phash_opt.as_deref());
        let ndef_file = hex::decode(&template.ndef_file_hex)
            .map_err(|e| format!("template hex decode: {e}"))?;

        // Resolve auth key: vault-derived if tag has key_version, else transport.
        // Enforce: only the enrolling identity can rewrite; released tags (identity_id = None) allow take control.
        let auth_key: [u8; 16] = if let Some(ref uid) = tag_uid {
            let uid_normalized = uid.trim().to_uppercase();
            if let Ok(Some(tag)) = crate::vault::get_tag_by_uid(vault_dir, master_password, &uid_normalized) {
                if let Some(tag_identity_id) = tag.identity_id {
                    if tag_identity_id != active_identity_id {
                        return Err(format!(
                            "tag is controlled by a different identity (identity_id={}); only the enrolling identity can rewrite",
                            tag_identity_id
                        ));
                    }
                }
                // key_version None = enrolled before key rotation; keys still at transport (all zeros)
                let kv = tag.key_version;
                if let Some(kv) = kv {
                    if kv > 0 {
                    let vault_secret = crate::vault::get_vault_secret_for_tag_derivation(vault_dir, master_password)
                        .map_err(|e| format!("vault secret: {e}"))?;
                    let uid_bytes = hex::decode(&uid_normalized)
                        .map_err(|e| format!("uid hex: {e}"))?;
                    if uid_bytes.len() == 7 {
                        let key = crate::vault_keys::derive_single_key(
                            vault_secret.as_ref(),
                            &uid_bytes,
                            kv as u32,
                            0,
                        )
                        .map_err(|e| format!("derive key: {e}"))?;
                        log.push(format!("Auth: derived key (key_version={kv})"));
                        key
                    } else {
                        log.push("Auth: transport key (UID length != 7)".into());
                        transport_key
                    }
                    } else {
                        log.push("Auth: transport key (key_version=0)".into());
                        transport_key
                    }
                } else {
                    log.push("Auth: transport key (enrolled before key rotation)".into());
                    transport_key
                }
            } else {
                log.push("Auth: transport key (tag not in vault)".into());
                transport_key
            }
        } else {
            log.push("Auth: transport key (no UID)".into());
            transport_key
        };

        // 1. SELECT NDEF app
        let sel_app = transmit(&iso7816_t4t::cmd_select_ndef_app())?;
        if !iso7816_t4t::is_iso_success(&sel_app) {
            return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel_app)));
        }
        log.push("SELECT NDEF app: OK".into());

        // 2. GetFileSettings(02) — before auth
        let sdm_enabled = {
            let mut on = false;
            let fs_resp = transmit(&ntag424::cmd_get_file_settings(0x02))?;
            if let Ok(fs_data) = ntag424::expect_success(&fs_resp) {
                if let Ok(fs) = ntag424::parse_file_settings(&fs_data) {
                    let comm = match fs.comm_mode {
                        ntag424::CommMode::Plain => "Plain",
                        ntag424::CommMode::Mac => "Mac",
                        ntag424::CommMode::Full => "Full",
                    };
                    log.push(format!("GetFileSettings(02): comm={} sdm={} size={} ar={:04X}",
                        comm, fs.sdm_enabled, fs.file_size, fs.access_rights));
                    on = fs.sdm_enabled;
                }
            }
            on
        };

        let success;
        if sdm_enabled {
            log.push("SDM enabled: auth + ChangeFileSettings + WriteData".into());
            let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
            let part1_data = ntag424::expect_additional_frame(&part1_resp)
                .map_err(|e| format!("Auth part1: {e}"))?;
            let challenge = ev2_auth::process_auth_part1(&part1_data, &auth_key, 0x00)
                .map_err(|e| format!("Auth crypto: {e}"))?;
            let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
            let part2_data = ntag424::expect_success(&part2_resp)
                .map_err(|e| format!("Auth part2: {e}"))?;
            let mut session = ev2_auth::complete_auth(challenge, &part2_data)
                .map_err(|e| format!("Auth complete: {e}"))?;
            log.push("Auth EV2: OK".into());

            let sdm_cfg = enrollment::build_enrollment_sdm_config(
                &template,
                0x01,
                0x02,
                None,
                true,
                true,
            );
            let cfs_plaintext = ntag424::build_sdm_file_settings(&sdm_cfg);
            let cfs_apdu = session.build_change_file_settings_command(&[0x02], &cfs_plaintext, true);
            let cfs_resp = transmit(&cfs_apdu)?;
            let cfs_ok = cfs_resp.len() >= 2
                && cfs_resp[cfs_resp.len() - 2] == 0x91
                && cfs_resp[cfs_resp.len() - 1] == 0x00;
            if cfs_ok {
                log.push("ChangeFileSettings: OK (SDMCtrRet=0x00)".into());
            } else {
                log.push(format!("ChangeFileSettings: failed ({})", hex::encode(&cfs_resp)));
            }

            let write_apdu = ntag424::cmd_write_data_plain(0x02, 0, &ndef_file);
            let write_resp = transmit(&write_apdu)?;
            let nxp = ntag424::parse_nxp_response(&write_resp);
            success = nxp.as_ref().map(|r| r.status.is_success()).unwrap_or(false);
            if !success {
                let status_hex = if write_resp.len() >= 2 {
                    format!("{:02X}{:02X}", write_resp[write_resp.len() - 2], write_resp[write_resp.len() - 1])
                } else {
                    "??".into()
                };
                log.push(format!("WriteData(8D): FAILED (response {})", status_hex));
            } else {
                log.push("WriteData(8D): OK".into());
            }
        } else {
            let sel_file = transmit(&iso7816_t4t::cmd_select_ndef_file())?;
            if !iso7816_t4t::is_iso_success(&sel_file) {
                return Err(format!("SELECT NDEF file failed: {}", hex::encode(&sel_file)));
            }
            log.push("SELECT NDEF file: OK".into());
            let update_apdu = iso7816_t4t::cmd_update_binary(0, &ndef_file);
            let update_resp = transmit(&update_apdu)?;
            success = iso7816_t4t::is_iso_success(&update_resp);
            log.push(if success { "UpdateBinary: OK".into() } else { "UpdateBinary: denied".into() });
        }

        let bytes_written = ndef_file.len();
        let mut read_back = None;
        let mut match_ok = None;
        if success {
            let read_apdu = ntag424::cmd_read_data_plain(0x02, 0, ndef_file.len() as u32);
            let read_resp = transmit(&read_apdu)?;
            if let Ok(data) = ntag424::expect_success(&read_resp) {
                read_back = Some(hex::encode(&data));
                let prefix_match = data.len() >= ndef_file.len() && data[..ndef_file.len()] == ndef_file[..];
                match_ok = Some(prefix_match);
            }
        }

        Ok(WriteNdefExecuteResult {
            success,
            bytes_written,
            read_back_hex: read_back,
            match_ok,
            log,
            tag_uid,
        })
    })
}

// ---------------------------------------------------------------------------
// rotate_tag_keys_execute — rotate keys on a pre-rotation tag
// ---------------------------------------------------------------------------

/// Result of key rotation on a pre-rotation tag.
#[derive(Debug, Clone, Serialize)]
pub struct RotateTagKeysResult {
    pub success: bool,
    pub tag_uid: String,
    pub key_version: u32,
    pub log: Vec<String>,
}

/// Rotate keys on a tag that was enrolled before key rotation.
/// Auth with transport key, then ChangeKey 01–04, then 00. Store key_version=1 in vault.
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn rotate_tag_keys_execute(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    vault_dir: &std::path::Path,
    master_password: &str,
    active_identity_id: i64,
) -> Result<RotateTagKeysResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;
    use crate::ntag424;

    let preflight = preflight_tag_state(
        preferred_reader,
        timeout_ms,
        Some(vault_dir),
        Some(master_password),
        Some(active_identity_id),
    )?;
    let tag_uid = preflight.tag_uid.trim();
    if tag_uid.is_empty() {
        return Err("preflight: no tag UID".to_string());
    }
    let uid_normalized = tag_uid.trim().to_uppercase();

    let tag = crate::vault::get_tag_by_uid(vault_dir, master_password, &uid_normalized)
        .map_err(|e| format!("lookup tag: {e}"))?
        .ok_or_else(|| "tag not found in vault".to_string())?;

    if tag.identity_id != Some(active_identity_id) {
        return Err("tag is not controlled by the active identity".to_string());
    }

    let is_pre_rotation = tag.key_version.is_none() || tag.key_version == Some(0);
    if !is_pre_rotation {
        return Err(format!(
            "tag already has rotated keys (key_version={}); no rotation needed",
            tag.key_version.unwrap_or(0)
        ));
    }

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let transport_key = crate::vault_keys::TRANSPORT_KEY;

    let vault_secret = crate::vault::get_vault_secret_for_tag_derivation(vault_dir, master_password)
        .map_err(|e| format!("vault secret: {e}"))?;
    let tag_uid_bytes = hex::decode(&uid_normalized).map_err(|e| format!("uid hex: {e}"))?;
    if tag_uid_bytes.len() != 7 {
        return Err(format!("tag UID must be 7 bytes, got {}", tag_uid_bytes.len()));
    }
    let mut uid_arr = [0u8; 7];
    uid_arr.copy_from_slice(&tag_uid_bytes);
    let key_version = 1u32;
    let key_set = crate::vault_keys::derive_tag_keys(vault_secret.as_ref(), &uid_arr, key_version);
    let key_version_u8 = key_version.min(255) as u8;

    conn.with_transaction(|transmit| {
        let sel = transmit(&iso7816_t4t::cmd_select_ndef_app())?;
        if !iso7816_t4t::is_iso_success(&sel) {
            return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel)));
        }
        log.push("SELECT NDEF app: OK".into());

        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, &transport_key, 0x00)
            .map_err(|e| format!("Auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Auth complete: {e}"))?;
        log.push("Auth EV2: OK (transport key)".into());

        for key_no in 1..=4 {
            let plaintext = enrollment::prepare_key_rotation(
                key_no,
                &key_set.keys[key_no as usize],
                &transport_key,
                key_version_u8,
                &session,
            );
            let first_ck = key_no == 1;
            let apdu = session.build_change_key_command(&[key_no], &plaintext, first_ck);
            let resp = transmit(&apdu)?;
            if resp.len() < 2 || resp[resp.len() - 2] != 0x91 || resp[resp.len() - 1] != 0x00 {
                return Err(format!("ChangeKey({key_no:02X}) failed: {}", hex::encode(&resp)));
            }
            log.push(format!("ChangeKey({key_no:02X}): OK"));
        }

        let plaintext = enrollment::prepare_key_rotation(
            0,
            &key_set.keys[0],
            &transport_key,
            key_version_u8,
            &session,
        );
        let apdu = session.build_change_key_command(&[0x00], &plaintext, false);
        let resp = transmit(&apdu)?;
        if resp.len() < 2 || resp[resp.len() - 2] != 0x91 || resp[resp.len() - 1] != 0x00 {
            return Err(format!("ChangeKey(00) failed: {}", hex::encode(&resp)));
        }
        log.push("ChangeKey(00): OK".into());

        Ok(RotateTagKeysResult {
            success: true,
            tag_uid: tag_uid.to_string(),
            key_version,
            log,
        })
    })
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn rotate_tag_keys_execute(
    _reader: Option<&str>,
    _timeout: u64,
    _vault_dir: &std::path::Path,
    _master_password: &str,
    _active_identity_id: i64,
) -> Result<RotateTagKeysResult, String> {
    Err("NFC not supported on this platform".into())
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn rewrite_tag_ndef_execute(
    _reader: Option<&str>,
    _timeout: u64,
    _base_url: &str,
    _vault_dir: &std::path::Path,
    _master_password: &str,
    _active_identity_id: i64,
) -> Result<WriteNdefExecuteResult, String> {
    Err("NFC not supported on this platform".into())
}

// ---------------------------------------------------------------------------
// debug_write_ndef_template_execute — write NDEF after ChangeFileSettings
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize)]
pub struct WriteNdefExecuteResult {
    pub success: bool,
    pub bytes_written: usize,
    pub read_back_hex: Option<String>,
    pub match_ok: Option<bool>,
    pub log: Vec<String>,
    /// UID of the physical tag that was written to (for vault association).
    pub tag_uid: Option<String>,
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn debug_write_ndef_template_execute(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    base_url: &str,
) -> Result<WriteNdefExecuteResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;
    use crate::ntag424;

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let policy = SdmTemplatePolicy::default();
    let zero_key = [0u8; 16];

    let template = enrollment::build_sdm_template(base_url, &policy, None, None, None, None);
    let ndef_file = hex::decode(&template.ndef_file_hex)
        .map_err(|e| format!("template hex decode: {e}"))?;

    conn.with_transaction(|transmit| {
        // NXP flow: all SELECTs before auth, then only protected commands. No SELECT after auth.

        // 0. Read UID of the physical tag we're connected to (for correct vault association)
        let tag_uid = {
            let uid_resp = transmit(&[0xFF, 0xCA, 0x00, 0x00, 0x00])?;
            if uid_resp.len() >= 2
                && uid_resp[uid_resp.len() - 2] == 0x90
                && uid_resp[uid_resp.len() - 1] == 0x00
            {
                let uid_bytes = &uid_resp[..uid_resp.len() - 2];
                Some(hex::encode_upper(uid_bytes))
            } else {
                None
            }
        };

        // 1. SELECT NDEF app
        let sel_app = transmit(&iso7816_t4t::cmd_select_ndef_app())?;
        if !iso7816_t4t::is_iso_success(&sel_app) {
            return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel_app)));
        }
        log.push("SELECT NDEF app: OK".into());

        // 2. GetFileSettings(02) — before auth
        let sdm_enabled = {
            let mut on = false;
            let fs_resp = transmit(&ntag424::cmd_get_file_settings(0x02))?;
            if let Ok(fs_data) = ntag424::expect_success(&fs_resp) {
                if let Ok(fs) = ntag424::parse_file_settings(&fs_data) {
                    let comm = match fs.comm_mode {
                        ntag424::CommMode::Plain => "Plain",
                        ntag424::CommMode::Mac => "Mac",
                        ntag424::CommMode::Full => "Full",
                    };
                    log.push(format!("GetFileSettings(02): comm={} sdm={} size={} ar={:04X}",
                        comm, fs.sdm_enabled, fs.file_size, fs.access_rights));
                    on = fs.sdm_enabled;
                }
            }
            on
        };

        // 3. If SDM enabled: auth + ChangeFileSettings + WriteData (no SELECT file — matches enrollment)
        //    If SDM disabled: try UpdateBinary (requires SELECT file)
        let success;
        if sdm_enabled {
            // SDM path: auth + ChangeFileSettings (SDMCtrRet=0x00) + WriteData. No SELECT file before auth.
            log.push("SDM enabled: auth + ChangeFileSettings + WriteData".into());
            let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
            let part1_data = ntag424::expect_additional_frame(&part1_resp)
                .map_err(|e| format!("Auth part1: {e}"))?;
            let challenge = ev2_auth::process_auth_part1(&part1_data, &zero_key, 0x00)
                .map_err(|e| format!("Auth crypto: {e}"))?;
            let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
            let part2_data = ntag424::expect_success(&part2_resp)
                .map_err(|e| format!("Auth part2: {e}"))?;
            let mut session = ev2_auth::complete_auth(challenge, &part2_data)
                .map_err(|e| format!("Auth complete: {e}"))?;
            log.push("Auth EV2: OK".into());

            // ChangeFileSettings: apply current SDM config (including SDMCtrRet=0x00 for counter read)
            let sdm_cfg = enrollment::build_enrollment_sdm_config(
                &template,
                0x01,
                0x02,
                None,
                true,  // nxp_compat
                true,  // force_nxp_offsets
            );
            let cfs_plaintext = ntag424::build_sdm_file_settings(&sdm_cfg);
            let cfs_apdu = session.build_change_file_settings_command(&[0x02], &cfs_plaintext, true);
            let cfs_resp = transmit(&cfs_apdu)?;
            let cfs_ok = cfs_resp.len() >= 2
                && cfs_resp[cfs_resp.len() - 2] == 0x91
                && cfs_resp[cfs_resp.len() - 1] == 0x00;
            if cfs_ok {
                log.push("ChangeFileSettings: OK (SDMCtrRet=0x00)".into());
            } else {
                log.push(format!("ChangeFileSettings: failed ({})", hex::encode(&cfs_resp)));
            }

            // File comm=Plain: use plain WriteData (header+data, no MAC). Lc=0x5D (93 bytes).
            let write_apdu = ntag424::cmd_write_data_plain(0x02, 0, &ndef_file);
            let apdu_hex = hex::encode(&write_apdu);
            log.push(format!("WriteData plain: {} len={} (Lc=0x5D)", &apdu_hex[..apdu_hex.len().min(80)], write_apdu.len()));
            let write_resp = transmit(&write_apdu)?;
            let nxp = ntag424::parse_nxp_response(&write_resp);
            success = nxp.as_ref().map(|r| r.status.is_success()).unwrap_or(false);
            if !success {
                let status_hex = if write_resp.len() >= 2 {
                    format!("{:02X}{:02X}", write_resp[write_resp.len() - 2], write_resp[write_resp.len() - 1])
                } else {
                    "??".into()
                };
                log.push(format!("WriteData(8D): FAILED (response {})", status_hex));
            } else {
                log.push("WriteData(8D): OK".into());
            }
        } else {
            // Plain path: try UpdateBinary (requires SELECT file)
            let sel_file = transmit(&iso7816_t4t::cmd_select_ndef_file())?;
            if !iso7816_t4t::is_iso_success(&sel_file) {
                return Err(format!("SELECT NDEF file failed: {}", hex::encode(&sel_file)));
            }
            log.push("SELECT NDEF file: OK".into());
            let update_apdu = iso7816_t4t::cmd_update_binary(0, &ndef_file);
            let update_resp = transmit(&update_apdu)?;
            success = iso7816_t4t::is_iso_success(&update_resp);
            log.push(if success { "UpdateBinary: OK".into() } else { "UpdateBinary: denied".into() });
        }

        let bytes_written = ndef_file.len();

        // Read back — use ReadData (file no 02) to avoid SELECT after auth
        let mut read_back = None;
        let mut match_ok = None;
        if success {
            let read_apdu = ntag424::cmd_read_data_plain(0x02, 0, ndef_file.len() as u32);
            let read_resp = transmit(&read_apdu)?;
            if let Ok(data) = ntag424::expect_success(&read_resp) {
                read_back = Some(hex::encode(&data));
                let prefix_match = data.len() >= ndef_file.len() && data[..ndef_file.len()] == ndef_file[..];
                match_ok = Some(prefix_match);
            }
        }

        Ok(WriteNdefExecuteResult {
            success,
            bytes_written,
            read_back_hex: read_back,
            match_ok,
            log,
            tag_uid,
        })
    })
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn debug_write_ndef_template_execute(
    _reader: Option<&str>,
    _timeout: u64,
    _base_url: &str,
) -> Result<WriteNdefExecuteResult, String> {
    Err("NFC not supported on this platform".into())
}

// ---------------------------------------------------------------------------
// reset_tag_sdm_execute — disable SDM on tag (recover from failed enrollment)
// ---------------------------------------------------------------------------

const FACTORY_CC_FILE: [u8; 32] = [
    0x00, 0x20, // CCLEN = 32
    0x20,       // mapping v2.0
    0x01, 0x00, // MLe = 256
    0x00, 0xFF, // MLc = 255
    0x04, 0x06, // NDEF File Control TLV
    0xE1, 0x04, // FID = E104
    0x01, 0x00, // max NDEF size = 256
    0x00,       // read access = free
    0x00,       // write access = free
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00,
];

const FACTORY_NDEF_FILE_SIZE: usize = 256;
const WRITE_DATA_PLAIN_MAX_PAYLOAD: usize = 248;

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn write_data_plain_chunked<F>(
    transmit: &mut F,
    file_no: u8,
    payload: &[u8],
    log: &mut Vec<String>,
    label: &str,
) -> Result<(), String>
where
    F: FnMut(&[u8]) -> Result<Vec<u8>, String> + ?Sized,
{
    for (idx, chunk) in payload.chunks(WRITE_DATA_PLAIN_MAX_PAYLOAD).enumerate() {
        let offset = (idx * WRITE_DATA_PLAIN_MAX_PAYLOAD) as u32;
        let apdu = ntag424::cmd_write_data_plain(file_no, offset, chunk);
        let resp = transmit(&apdu)?;
        let nxp = ntag424::parse_nxp_response(&resp)
            .ok_or_else(|| format!("{label}: not an NXP response: {}", hex::encode(&resp)))?;
        if !nxp.status.is_success() {
            return Err(format!("{label}: {}", nxp.status));
        }
    }
    log.push(format!("{label}: OK"));
    Ok(())
}

/// Disable SDM on a tag that has SDM enabled but still uses transport keys.
/// Use when a bind failed partway and the tag still authenticates with the transport key.
/// After success, the tag is restored as close to delivery state as transport-key access allows.
#[derive(Debug, Clone, Serialize)]
pub struct ResetTagSdmResult {
    pub success: bool,
    pub tag_uid: String,
    pub log: Vec<String>,
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn reset_tag_sdm_execute(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    vault_dir: &std::path::Path,
    master_password: &str,
    active_identity_id: i64,
) -> Result<ResetTagSdmResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;

    let preflight = preflight_tag_state(
        preferred_reader,
        timeout_ms,
        Some(vault_dir),
        Some(master_password),
        Some(active_identity_id),
    )?;
    let tag_uid = preflight.tag_uid.trim().to_string();
    if tag_uid.is_empty() {
        return Err("preflight: no tag UID".to_string());
    }
    if preflight.classification != "sdm_enabled" {
        return Err(format!(
            "tag is not sdm_enabled (classification: {}). Reset is only available for tags with SDM enabled.",
            preflight.classification
        ));
    }

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let transport_key = crate::vault_keys::TRANSPORT_KEY;
    let factory_ndef = vec![0u8; FACTORY_NDEF_FILE_SIZE];

    let mut reset_key_set: Option<crate::vault_keys::TagKeySet> = None;
    let auth_key: [u8; 16] = if let Ok(Some(tag)) =
        crate::vault::get_tag_by_uid(vault_dir, master_password, &tag_uid)
    {
        if let Some(tag_identity_id) = tag.identity_id {
            if tag_identity_id != active_identity_id {
                return Err(format!(
                    "tag is controlled by a different identity (identity_id={}); only the enrolling identity can reset it",
                    tag_identity_id
                ));
            }
        }
        if let Some(kv) = tag.key_version {
            if kv > 0 {
                let vault_secret = crate::vault::get_vault_secret_for_tag_derivation(vault_dir, master_password)
                    .map_err(|e| format!("vault secret: {e}"))?;
                let uid_bytes = hex::decode(&tag_uid).map_err(|e| format!("uid hex: {e}"))?;
                if uid_bytes.len() == 7 {
                    let mut uid_arr = [0u8; 7];
                    uid_arr.copy_from_slice(&uid_bytes);
                    let key_set = crate::vault_keys::derive_tag_keys(
                        vault_secret.as_ref(), &uid_arr, kv as u32,
                    );
                    let admin_key = key_set.keys[0];
                    reset_key_set = Some(key_set);
                    log.push(format!("Auth: derived key (key_version={kv})"));
                    admin_key
                } else {
                    log.push("Auth: transport key (UID length != 7)".into());
                    transport_key
                }
            } else {
                log.push("Auth: transport key (key_version=0)".into());
                transport_key
            }
        } else {
            log.push("Auth: transport key (no key_version)".into());
            transport_key
        }
    } else {
        log.push("Auth: transport key (tag not in vault)".into());
        transport_key
    };

    conn.with_transaction(|transmit| {
        let sel = transmit(&iso7816_t4t::cmd_select_ndef_app())?;
        if !iso7816_t4t::is_iso_success(&sel) {
            return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel)));
        }
        log.push("SELECT NDEF app: OK".into());

        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, &auth_key, 0x00)
            .map_err(|e| format!("Auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Auth complete: {e}"))?;
        log.push("Auth EV2: OK".into());

        // Restore delivery-like file settings on the NDEF file: plain comm, free read, key0 write/change.
        let plaintext: Vec<u8> = vec![0x00, 0x00, 0xE0];
        let apdu = session.build_change_file_settings_command(&[0x02], &plaintext, true);
        let response = transmit(&apdu)?;
        let cfs_ok = response.len() >= 2
            && response[response.len() - 2] == 0x91
            && response[response.len() - 1] == 0x00;
        if !cfs_ok {
            return Err(format!("ChangeFileSettings failed: {}", hex::encode(&response)));
        }
        log.push("ChangeFileSettings (delivery-like NDEF settings): OK".into());

        // Clear NDEF and restore CC file while transport-key auth is still active.
        write_data_plain_chunked(transmit, 0x02, &factory_ndef, &mut log, "WriteData NDEF clear")?;
        write_data_plain_chunked(transmit, 0x01, &FACTORY_CC_FILE, &mut log, "WriteData CC restore")?;

        // Re-auth before ChangeKey: plain writes still advance CmdCtr while authenticated.
        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Re-auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, &auth_key, 0x00)
            .map_err(|e| format!("Re-auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Re-auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Re-auth complete: {e}"))?;
        log.push("Auth EV2 before key reset: OK".into());

        // Reset keys 01–04 back to transport zero key and key version 0.
        // old_key must be the per-slot derived key currently on the tag.
        for key_no in 1..=4 {
            let old_key = reset_key_set.as_ref()
                .map(|ks| ks.keys[key_no as usize])
                .unwrap_or(transport_key);
            let plaintext = enrollment::prepare_key_rotation(
                key_no,
                &transport_key,
                &old_key,
                0x00,
                &session,
            );
            let first_ck = key_no == 1;
            let apdu = session.build_change_key_command(&[key_no], &plaintext, first_ck);
            let resp = transmit(&apdu)?;
            if resp.len() < 2 || resp[resp.len() - 2] != 0x91 || resp[resp.len() - 1] != 0x00 {
                return Err(format!(
                    "ChangeKey({key_no:02X}) reset failed: {}. The tag may no longer be fully on transport keys.",
                    hex::encode(&resp)
                ));
            }
            log.push(format!("ChangeKey({key_no:02X}) -> transport: OK"));
        }

        // Reset key 00 last (self-change: new_key = transport).
        let plaintext = enrollment::prepare_key_rotation(0x00, &transport_key, &auth_key, 0x00, &session);
        let apdu = session.build_change_key_command(&[0x00], &plaintext, false);
        let resp = transmit(&apdu)?;
        if resp.len() < 2 || resp[resp.len() - 2] != 0x91 || resp[resp.len() - 1] != 0x00 {
            return Err(format!(
                "ChangeKey(00) reset failed: {}. The tag may no longer be fully on transport keys.",
                hex::encode(&resp)
            ));
        }
        log.push("ChangeKey(00) -> transport: OK".into());

        Ok(ResetTagSdmResult {
            success: true,
            tag_uid,
            log,
        })
    })
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn reset_tag_sdm_execute(
    _reader: Option<&str>,
    _timeout: u64,
    _vault_dir: &std::path::Path,
    _master_password: &str,
    _active_identity_id: i64,
) -> Result<ResetTagSdmResult, String> {
    Err("NFC not supported on this platform".into())
}

// ---------------------------------------------------------------------------
// enroll_tag_execute — full enrollment: Auth + ChangeFileSettings + Write NDEF
// ---------------------------------------------------------------------------

/// Result of identity-bound enrollment. Tag UID and key_version on success.
#[derive(Debug, Clone, Serialize)]
pub struct EnrollTagResult {
    pub success: bool,
    pub tag_uid: String,
    /// Key version stored on chip after rotation (1 for first enrollment).
    pub key_version: u32,
    pub log: Vec<String>,
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn enroll_tag_execute(
    preferred_reader: Option<&str>,
    timeout_ms: u64,
    verification_url: &str,
    vault_dir: &std::path::Path,
    master_password: &str,
) -> Result<EnrollTagResult, String> {
    use crate::ev2_auth;
    use crate::iso7816_t4t;
    use crate::nfc;

    // 1. Preflight to validate tag state
    let preflight = preflight_tag_state(
        preferred_reader,
        timeout_ms,
        Some(vault_dir),
        Some(master_password),
        None,
    )?;
    let tag_uid = preflight.tag_uid.trim();
    if tag_uid.is_empty() {
        return Err("preflight: no tag UID".to_string());
    }
    let classification = preflight.classification.as_str();
    if classification != "plain_ndef" && classification != "factory_default_likely" {
        return Err(format!(
            "tag is not suitable for enrollment (classification: {}). Use a factory-default or plain NDEF tag.",
            classification
        ));
    }

    let mut conn = nfc::connect_tag(preferred_reader, timeout_ms)?;
    let mut log = Vec::new();
    let zero_key = [0u8; 16];
    let policy = SdmTemplatePolicy::default();

    let template = enrollment::build_sdm_template(verification_url, &policy, None, None, None, None);
    let ndef_file = hex::decode(&template.ndef_file_hex)
        .map_err(|e| format!("template hex decode: {e}"))?;

    let sdm_cfg = enrollment::build_enrollment_sdm_config(
        &template,
        0x01,
        0x02,
        None,
        true,  // nxp_compat
        true,  // force_nxp_offsets (32, 67, 67)
    );
    let plaintext = ntag424::build_sdm_file_settings(&sdm_cfg);

    conn.with_transaction(|transmit| {
        // SELECT NDEF app
        let sel = transmit(&iso7816_t4t::cmd_select_ndef_app())?;
        if !iso7816_t4t::is_iso_success(&sel) {
            return Err(format!("SELECT NDEF app failed: {}", hex::encode(&sel)));
        }
        log.push("SELECT NDEF app: OK".into());

        // Auth EV2 First
        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, &zero_key, 0x00)
            .map_err(|e| format!("Auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Auth complete: {e}"))?;
        log.push("Auth EV2: OK".into());

        // ChangeFileSettings (CmdCtr=0 for first command)
        let apdu = session.build_change_file_settings_command(&[0x02], &plaintext, true);
        let response = transmit(&apdu)?;
        let (cfs_ok, _) = if response.len() >= 2 {
            let sw1 = response[response.len() - 2];
            let sw2 = response[response.len() - 1];
            (sw1 == 0x91 && sw2 == 0x00, format!("{sw1:02X}{sw2:02X}"))
        } else {
            (false, "invalid".into())
        };
        if !cfs_ok {
            return Err(format!("ChangeFileSettings failed: {}", hex::encode(&response)));
        }
        log.push("ChangeFileSettings: OK".into());

        // Write NDEF (plain WriteData, no SELECT after auth)
        let write_apdu = ntag424::cmd_write_data_plain(0x02, 0, &ndef_file);
        let write_resp = transmit(&write_apdu)?;
        let nxp = ntag424::parse_nxp_response(&write_resp);
        let write_ok = nxp.as_ref().map(|r| r.status.is_success()).unwrap_or(false);
        if !write_ok {
            return Err(format!("WriteData failed: {}", hex::encode(&write_resp)));
        }
        log.push("WriteData NDEF: OK".into());

        // Readback verify
        let read_apdu = ntag424::cmd_read_data_plain(0x02, 0, ndef_file.len() as u32);
        let read_resp = transmit(&read_apdu)?;
        let read_ok = if let Ok(data) = ntag424::expect_success(&read_resp) {
            data.len() >= ndef_file.len() && data[..ndef_file.len()] == ndef_file[..]
        } else {
            false
        };
        if !read_ok {
            log.push("Readback: mismatch (continuing)".into());
        } else {
            log.push("Readback: OK".into());
        }

        // Key rotation: derive TagKeySet, then re-auth before ChangeKey.
        // While the tag remains authenticated, even CommMode.Plain commands still
        // advance CmdCtr. Re-auth avoids counter drift from the prior WriteData /
        // ReadData steps and gives ChangeKey a fresh, known session state.
        let vault_secret = crate::vault::get_vault_secret_for_tag_derivation(vault_dir, master_password)
            .map_err(|e| format!("vault secret: {e}"))?;
        let tag_uid_bytes = hex::decode(tag_uid)
            .map_err(|e| format!("tag UID hex: {e}"))?;
        if tag_uid_bytes.len() != 7 {
            return Err(format!("tag UID must be 7 bytes, got {}", tag_uid_bytes.len()));
        }
        let mut uid_arr = [0u8; 7];
        uid_arr.copy_from_slice(&tag_uid_bytes);
        let key_version = 1u32;
        let key_set = crate::vault_keys::derive_tag_keys(vault_secret.as_ref(), &uid_arr, key_version);

        let transport_key = crate::vault_keys::TRANSPORT_KEY;
        let key_version_u8 = key_version.min(255) as u8;

        let part1_resp = transmit(&ntag424::cmd_auth_ev2_first_part1(0x00))?;
        let part1_data = ntag424::expect_additional_frame(&part1_resp)
            .map_err(|e| format!("Re-auth part1: {e}"))?;
        let challenge = ev2_auth::process_auth_part1(&part1_data, &transport_key, 0x00)
            .map_err(|e| format!("Re-auth crypto: {e}"))?;
        let part2_resp = transmit(&ntag424::cmd_auth_ev2_first_part2(&challenge.part2_payload))?;
        let part2_data = ntag424::expect_success(&part2_resp)
            .map_err(|e| format!("Re-auth part2: {e}"))?;
        let mut session = ev2_auth::complete_auth(challenge, &part2_data)
            .map_err(|e| format!("Re-auth complete: {e}"))?;
        log.push("Auth EV2 before key rotation: OK".into());

        // Rotate keys 01–04 (old = transport, new = derived)
        // Compact format (41 bytes). First ChangeKey is first command in the
        // fresh session, so it must use CmdCtr=0.
        for key_no in 1..=4 {
            let plaintext = enrollment::prepare_key_rotation(
                key_no,
                &key_set.keys[key_no as usize],
                &transport_key,
                key_version_u8,
                &session,
            );
            let first_ck = key_no == 1;
            let apdu = session.build_change_key_command(&[key_no], &plaintext, first_ck);
            let resp = transmit(&apdu)?;
            if resp.len() < 2 || resp[resp.len() - 2] != 0x91 || resp[resp.len() - 1] != 0x00 {
                return Err(format!(
                    "ChangeKey({key_no:02X}) failed: {}",
                    hex::encode(&resp)
                ));
            }
            log.push(format!("ChangeKey({key_no:02X}): OK"));
        }

        // Rotate key 00 last (self-change)
        let plaintext = enrollment::prepare_key_rotation(
            0,
            &key_set.keys[0],
            &transport_key,
            key_version_u8,
            &session,
        );
        let apdu = session.build_change_key_command(&[0x00], &plaintext, false);
        let resp = transmit(&apdu)?;
        if resp.len() < 2 || resp[resp.len() - 2] != 0x91 || resp[resp.len() - 1] != 0x00 {
            return Err(format!("ChangeKey(00) failed: {}", hex::encode(&resp)));
        }
        log.push("ChangeKey(00): OK".into());

        Ok(EnrollTagResult {
            success: true,
            tag_uid: tag_uid.to_string(),
            key_version,
            log,
        })
    })
}

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
pub fn enroll_tag_execute(
    _reader: Option<&str>,
    _timeout: u64,
    _verification_url: &str,
    _vault_dir: &std::path::Path,
    _master_password: &str,
) -> Result<EnrollTagResult, String> {
    Err("NFC not supported on this platform".into())
}

// ---------------------------------------------------------------------------
// debug_verify_sdm_layout — pure local offset verification
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize)]
pub struct VerifySdmLayoutResult {
    pub ok: bool,
    pub checks: Vec<String>,
    pub errors: Vec<String>,
}

/// Verify that configured offsets land on the intended placeholder regions in the NDEF bytes.
pub fn debug_verify_sdm_layout(
    ndef_file_hex: &str,
    picc_data_offset: Option<u32>,
    mac_input_offset: u32,
    mac_offset: u32,
) -> VerifySdmLayoutResult {
    let ndef_bytes = match hex::decode(ndef_file_hex) {
        Ok(b) => b,
        Err(_) => {
            return VerifySdmLayoutResult {
                ok: false,
                checks: vec![],
                errors: vec!["invalid ndef_file_hex".into()],
            };
        }
    };

    let mut checks = Vec::new();
    let mut errors = Vec::new();

    // NLEN at bytes 0..2
    if ndef_bytes.len() >= 2 {
        let nlen = u16::from_be_bytes([ndef_bytes[0], ndef_bytes[1]]);
        checks.push(format!("NLEN={nlen}"));
    }

    // URI body starts at byte 7 (after D1 01 len U prefix_code)
    let uri_body_start = 7u32;
    if ndef_bytes.len() < 8 {
        errors.push("NDEF file too short".into());
        return VerifySdmLayoutResult { ok: false, checks, errors };
    }

    // picc_data= value offset: search for "picc_data=" in URI body (bytes 7..)
    let uri_body = &ndef_bytes[uri_body_start as usize..];
    if let Some(off) = picc_data_offset {
        if let Some(pos) = uri_body.windows(10).position(|w| w == b"picc_data=") {
            let expected = uri_body_start + pos as u32 + 10;
            if off == expected {
                checks.push(format!("picc_data_offset {off} points to first hex digit of picc_data value"));
            } else {
                errors.push(format!("picc_data_offset {off} != expected {expected}"));
            }
        } else {
            errors.push("picc_data= not found in URI body".into());
        }
        if (off as usize) + 32 <= ndef_bytes.len() {
            checks.push("picc_data region (32 hex chars) fits in file".into());
        } else {
            errors.push("picc_data region overflows".into());
        }
    }

    // cmac value offset: "cmac=" or "&c=" (NXP-valid layout)
    let cmac_expected = if let Some(pos) = uri_body.windows(5).position(|w| w == b"cmac=") {
        Some(uri_body_start + pos as u32 + 5)
    } else if let Some(pos) = uri_body.windows(3).position(|w| w == b"&c=") {
        Some(uri_body_start + pos as u32 + 3)
    } else {
        None
    };
    if let Some(expected) = cmac_expected {
        if mac_offset == expected {
            checks.push(format!("mac_offset {mac_offset} points to first hex digit of cmac value"));
        } else {
            errors.push(format!("mac_offset {mac_offset} != expected {expected}"));
        }
    } else {
        errors.push("cmac= or &c= not found in URI body".into());
    }
    if (mac_offset as usize) + 16 <= ndef_bytes.len() {
        checks.push("cmac region (16 hex chars) fits in file".into());
    } else {
        errors.push("cmac region overflows".into());
    }

    // mac_input_offset: NXP layout uses 67; legacy used uri_body_start (7)
    if mac_input_offset == uri_body_start {
        checks.push(format!("mac_input_offset {mac_input_offset} = URI body start"));
    } else if mac_input_offset == mac_offset {
        checks.push(format!("mac_input_offset {mac_input_offset} = mac_offset (NXP layout)"));
    } else {
        errors.push(format!("mac_input_offset {mac_input_offset} unexpected (expected {uri_body_start} or {mac_offset})"));
    }

    // Ordering: picc_data < mac_offset, mac_input < mac_offset
    if let Some(picc) = picc_data_offset {
        if picc >= mac_offset {
            errors.push("picc_data_offset >= mac_offset".into());
        }
    }
    if mac_input_offset > mac_offset {
        errors.push("mac_input_offset > mac_offset".into());
    }

    let ok = errors.is_empty();
    VerifySdmLayoutResult { ok, checks, errors }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn template_builder_default_url() {
        let policy = SdmTemplatePolicy::default();
        let result = debug_build_sdm_template("https://charm.farm/tap", &policy);
        assert!(result.url_preview.starts_with("https://charm.farm/tap"));
        assert!(result.uid_offset.is_some(), "plaintext default must have uid_offset");
        assert!(result.read_ctr_offset.is_some(), "plaintext default must have read_ctr_offset");
        assert!(result.picc_data_offset.is_none(), "plaintext default must not have picc_data_offset");
        assert_eq!(result.uri_prefix_code, 0x04);
        assert!(result.nlen > 0);
        assert!(result.ndef_file_len <= 256);
    }

    #[test]
    fn dry_run_produces_valid_output() {
        let policy = SdmTemplatePolicy::default();
        let result = dry_run_change_file_settings(
            "https://charm.farm/tap",
            &policy,
            0x01, // meta read key
            0x02, // file read key
        );
        assert!(!result.plaintext_hex.is_empty());
        assert!(!result.apdu_hex.is_empty());
        assert!(result.apdu_hex.starts_with("905f")); // 90 5F = ChangeFileSettings
        assert!(result.parsed_settings.len() >= 5);
    }

    #[test]
    fn dry_run_no_fatal_warnings() {
        let policy = SdmTemplatePolicy::default();
        let result = dry_run_change_file_settings(
            "https://charm.farm/tap",
            &policy,
            0x01,
            0x02,
        );
        // Padding note is informational (padding applied at encrypt time).
        // There should be no offset-ordering or size-limit warnings.
        let fatal: Vec<_> = result.warnings.iter()
            .filter(|w| w.contains("mac_offset") || w.contains("PICCData") || w.contains("exceeds"))
            .collect();
        assert!(fatal.is_empty(), "fatal warnings: {fatal:?}");
    }

    #[test]
    fn classify_factory_default() {
        let ver = Some(VersionSummary {
            hw_vendor: "04".into(),
            hw_type: "04".into(),
            hw_subtype: "05".into(),
            hw_version: "1.0".into(),
            sw_version: "1.0".into(),
            storage_size: 0x16,
            uid: "04010203040506".into(),
            batch: "0000000000".into(),
            is_ntag424_dna: true,
        });
        let fs = Some(FileSettingsSummary {
            file_type: "00".into(),
            comm_mode: "Plain".into(),
            sdm_enabled: false,
            access_rights_hex: "00E0".into(),
            file_size: 256,
            raw_hex: "00000000000100".into(),
        });
        let ndef = Some(NdefSummary {
            ndef_len: 0,
            is_empty: true,
            raw_prefix_hex: "0000".into(),
        });
        assert_eq!(classify_tag(&ver, &fs, &ndef), "factory_default_likely");
    }

    #[test]
    fn verify_sdm_layout_passes_for_default_template() {
        let policy = SdmTemplatePolicy::default();
        let template = debug_build_sdm_template("https://charm.farm/tap", &policy);
        let result = debug_verify_sdm_layout(
            &template.ndef_file_hex,
            template.picc_data_offset,
            template.mac_input_offset,
            template.mac_offset,
        );
        assert!(result.ok, "verify should pass: {:?}", result.errors);
    }

    #[test]
    fn classify_sdm_enabled() {
        let ver = Some(VersionSummary {
            hw_vendor: "04".into(), hw_type: "04".into(), hw_subtype: "05".into(),
            hw_version: "1.0".into(), sw_version: "1.0".into(), storage_size: 0x16,
            uid: "04010203040506".into(), batch: "0000000000".into(),
            is_ntag424_dna: true,
        });
        let fs = Some(FileSettingsSummary {
            file_type: "00".into(), comm_mode: "Plain".into(),
            sdm_enabled: true,
            access_rights_hex: "00E0".into(), file_size: 256,
            raw_hex: "004000E0000100".into(),
        });
        assert_eq!(classify_tag(&ver, &fs, &None), "sdm_enabled");
    }

    // -----------------------------------------------------------------------
    // Integration tests — require NFC reader + NTAG 424 DNA tag.
    // Run with: cargo test -- --ignored --test-threads=1
    // (Sequential run avoids reader contention; place tag on reader before starting.)
    // -----------------------------------------------------------------------

    #[test]
    #[ignore = "requires NFC reader and NTAG 424 DNA tag"]
    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
    fn integration_auth_ev2_first() {
        let result = debug_auth_ev2_first(None, 12000).expect("connect");
        assert!(result.part1_success, "Auth part1 failed: {:?}", result.log);
        assert!(result.part2_success, "Auth part2 failed: {:?}", result.log);
        assert_eq!(result.final_status, "success");
    }

    #[test]
    #[ignore = "requires NFC reader and NTAG 424 DNA tag (factory default)"]
    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
    fn integration_change_file_settings_nxp_example() {
        let result = debug_change_file_settings_execute(None, 12000, "https://charm.farm/tap", true, false, false, true)
            .expect("connect");
        assert!(result.success, "ChangeFileSettings failed: {} {:?}", result.response_status, result.log);
    }

    #[test]
    #[ignore = "requires NFC reader and NTAG 424 DNA tag (SDM already enabled)"]
    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
    fn integration_write_ndef_template() {
        let result = debug_write_ndef_template_execute(None, 12000, "https://charm.farm/tap")
            .expect("connect");
        assert!(result.success, "WriteData failed: {:?}", result.log);
        assert_eq!(result.match_ok, Some(true), "read-back verify failed");
    }
}
