//! NTAG 424 DNA enrollment state machine.
//!
//! Implements the safety-first provisioning ceremony:
//!   inspect → auth → configure SDM → write NDEF → rotate keys → verify.
//!
//! Each step returns a result that must be checked before proceeding.
//! The state machine prevents out-of-order operations and ensures no
//! irreversible action is taken without prior verification.

use crate::ev2_auth::Ev2Session;
use crate::ntag424;

// ---------------------------------------------------------------------------
// SDM template builder (used by nfc_debug, enroll_tag, rewrite, etc.)
// ---------------------------------------------------------------------------

/// Comprehensive result of the SDM URL template builder.
/// All offsets are relative to byte 0 of the NDEF file (the NLEN high byte).
#[derive(Debug, Clone, serde::Serialize)]
pub struct SdmTemplateResult {
    /// The complete NDEF URI record bytes (without NLEN prefix).
    pub ndef_record_hex: String,
    /// The full NDEF file content (NLEN + record).
    pub ndef_file_hex: String,
    /// NLEN value (NDEF message length, excluding the 2-byte NLEN prefix).
    pub nlen: u16,
    /// Total NDEF file byte count (NLEN prefix + message).
    pub ndef_file_len: usize,
    /// The full URL as it appears when a phone reads the tag (with placeholders).
    pub url_preview: String,
    /// URI prefix code used in the NDEF record (e.g. 0x04 = "https://").
    pub uri_prefix_code: u8,
    /// The URI body stored in the NDEF record (after the prefix code byte).
    pub uri_body: String,
    /// Offset of PICCData placeholder from start of NDEF file.
    /// `None` if plaintext mode (no encrypted PICC data).
    pub picc_data_offset: Option<u32>,
    /// Offset of plaintext UID mirror. `None` if encrypted PICC mode.
    pub uid_offset: Option<u32>,
    /// Offset of plaintext ReadCtr mirror. `None` if encrypted PICC mode.
    pub read_ctr_offset: Option<u32>,
    /// Offset from which SDMMAC input data begins.
    pub mac_input_offset: u32,
    /// Offset where the SDMMAC value is placed.
    pub mac_offset: u32,
}

/// SDM template builder policy flags.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct SdmTemplatePolicy {
    /// Use encrypted PICC data (recommended). If false, use plaintext UID + counter mirrors.
    #[serde(default = "default_true")]
    pub encrypted_picc: bool,
    /// Placeholder character for mirrored fields ('0' = zeros, 'x' = 0x-style).
    #[serde(default = "default_placeholder")]
    pub placeholder_char: char,
}

fn default_true() -> bool { true }
fn default_placeholder() -> char { '0' }

impl Default for SdmTemplatePolicy {
    fn default() -> Self {
        Self {
            encrypted_picc: false,
            placeholder_char: '0',
        }
    }
}

/// Detect the NFC Forum URI prefix code from a full URL.
/// Returns (prefix_code, body_after_prefix).
fn detect_uri_prefix(url: &str) -> (u8, &str) {
    if let Some(rest) = url.strip_prefix("https://www.") { return (0x02, rest); }
    if let Some(rest) = url.strip_prefix("http://www.") { return (0x01, rest); }
    if let Some(rest) = url.strip_prefix("https://") { return (0x04, rest); }
    if let Some(rest) = url.strip_prefix("http://") { return (0x03, rest); }
    (0x00, url)
}

/// Map prefix code back to scheme string for URL preview.
fn prefix_to_scheme(code: u8) -> &'static str {
    match code {
        0x01 => "http://www.",
        0x02 => "https://www.",
        0x03 => "http://",
        0x04 => "https://",
        _ => "",
    }
}

/// Strip the `xmp:iid:` prefix from a C2PA instance ID.
/// Returns only the UUID portion for compact storage on the NFC tag.
fn strip_instance_id_prefix(instance_id: &str) -> &str {
    instance_id
        .strip_prefix("xmp:iid:")
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .unwrap_or(instance_id)
}

/// Target offsets for NXP-valid SDM layout (AN12196: no mirror overlap).
/// mac_input and mac at 67 for encrypted PICC mode.
const TARGET_MAC_OFFSET: u32 = 67;

/// Pad string to exact length with trailing character.
fn pad_to_len(s: &str, len: usize, pad_char: char) -> String {
    if s.len() >= len {
        s[..len].to_string()
    } else {
        format!("{}{}", s, pad_char.to_string().repeat(len - s.len()))
    }
}

/// Build an SDM URL template with NXP-valid placeholder positions (32, 67, 67).
///
/// The URI body is structured so picc_data lands at byte 32 and cmac at byte 67.
/// url_after_scheme is padded to 15 chars to hit picc_data= at 32.
/// Uses "&c=" (3 bytes) before cmac to fit the 4-byte gap; backend accepts "c" as cmac alias.
///
/// `base_url`: full URL including scheme (e.g. "https://charm.farm/tap").
/// `policy`: SDM template policy flags.
/// `art_id`: optional IPFS CID to append as `&art_id={cid}` (plaintext, written once).
/// `instance_id`: optional C2PA manifest instance ID. The `xmp:iid:` prefix is stripped; only the UUID is stored as `&iid={uuid}` to save bytes.
/// `claim_hash`: optional first 8 bytes of SHA-256(claim_urn) as 16-char hex, stored as `&ch={hex}`.
/// `phash`: optional perceptual hash to append as `&phash={hex}` (plaintext, written once).
///
/// All offsets in the result are measured from byte 0 of the NDEF file.
pub fn build_sdm_template(
    base_url: &str,
    policy: &SdmTemplatePolicy,
    art_id: Option<&str>,
    instance_id: Option<&str>,
    claim_hash: Option<&str>,
    phash: Option<&str>,
) -> SdmTemplateResult {
    let (prefix_code, url_after_scheme) = detect_uri_prefix(base_url);

    let p = policy.placeholder_char;
    let picc_placeholder: String = std::iter::repeat(p).take(32).collect(); // 16 bytes = 32 hex
    let cmac_placeholder: String = std::iter::repeat(p).take(16).collect(); // 8 bytes = 16 hex
    let uid_placeholder: String = std::iter::repeat(p).take(14).collect();  // 7 bytes = 14 hex
    let ctr_placeholder: String = std::iter::repeat(p).take(6).collect();   // 3 bytes = 6 hex

    // NXP-valid layout: picc_data at 32, cmac at 67.
    // uri_body_file_offset=7, so we need 25 bytes before picc_data value: url + "?picc_data=".
    // 25 - 11 = 14 chars for url_after_scheme (11 = "?" + "picc_data=").
    let url_padded = pad_to_len(url_after_scheme, 14, 'x');
    // Between picc_data (32 bytes) and cmac: 64-66 = 3 bytes. Use "&c=" (backend accepts "c" as cmac).
    let mut uri_body = if policy.encrypted_picc {
        format!(
            "{url_padded}?picc_data={picc_placeholder}&c={cmac_placeholder}"
        )
    } else {
        format!(
            "{url_after_scheme}?uid={uid_placeholder}&ctr={ctr_placeholder}&cmac={cmac_placeholder}&v=1"
        )
    };

    // Append plaintext art_id (IPFS CID) and instance UUID (stripped from xmp:iid:) when provided.
    if let Some(c) = art_id.map(str::trim).filter(|s| !s.is_empty()) {
        uri_body.push_str(&format!("&art_id={}", urlencoding::encode(c)));
    }
    if let Some(u) = instance_id.map(str::trim).filter(|s| !s.is_empty()) {
        let uuid = strip_instance_id_prefix(u);
        if !uuid.is_empty() {
            uri_body.push_str(&format!("&iid={}", urlencoding::encode(uuid)));
        }
    }
    if let Some(ch) = claim_hash.map(str::trim).filter(|s| !s.is_empty()) {
        uri_body.push_str(&format!("&ch={}", urlencoding::encode(ch)));
    }
    if let Some(ph) = phash.map(str::trim).filter(|s| !s.is_empty()) {
        uri_body.push_str(&format!("&phash={}", urlencoding::encode(ph)));
    }

    // Full URL as a phone would see it.
    let scheme = prefix_to_scheme(prefix_code);
    let url_preview = format!("{scheme}{uri_body}");

    // Build NDEF URI record: [D1][01][payload_len][U][prefix_code][uri_body bytes]
    let payload_len = 1 + uri_body.len(); // prefix_code byte + uri_body
    if payload_len > 255 {
        // Short-record format only supports payload_len <= 255.
        // For NTAG 424 DNA with 256-byte NDEF file this is the practical limit.
        panic!("SDM URL template too long for short NDEF record: {payload_len} bytes");
    }
    let mut ndef_record = Vec::with_capacity(4 + payload_len);
    ndef_record.push(0xD1); // MB=1, ME=1, CF=0, SR=1, IL=0, TNF=0x01
    ndef_record.push(0x01); // Type length = 1
    ndef_record.push(payload_len as u8);
    ndef_record.push(b'U');  // Type = URI
    ndef_record.push(prefix_code);
    ndef_record.extend_from_slice(uri_body.as_bytes());

    let nlen = ndef_record.len() as u16;
    let mut ndef_file = Vec::with_capacity(2 + ndef_record.len());
    ndef_file.extend_from_slice(&nlen.to_be_bytes());
    ndef_file.extend_from_slice(&ndef_record);

    // NDEF file byte layout:
    //   [0..2]  NLEN (2 bytes)
    //   [2]     record header (0xD1)
    //   [3]     type length (0x01)
    //   [4]     payload length
    //   [5]     type ('U')
    //   [6]     prefix code
    //   [7..]   uri_body bytes
    let uri_body_file_offset = 7u32;

    // Find value offsets by locating the parameter name, then adding its length.
    // This avoids substring collisions between placeholders of different lengths.
    let find_value_offset = |param_name: &str| -> u32 {
        let pos = uri_body.find(param_name)
            .unwrap_or_else(|| panic!("parameter '{param_name}' not found in URI body"));
        uri_body_file_offset + (pos + param_name.len()) as u32
    };

    let (picc_data_offset, uid_offset, read_ctr_offset, mac_offset, mac_input_offset);
    if policy.encrypted_picc {
        picc_data_offset = Some(find_value_offset("picc_data="));
        uid_offset = None;
        read_ctr_offset = None;
        mac_offset = find_value_offset("c="); // "&c=" for NXP-valid layout (3-byte connector)
        mac_input_offset = TARGET_MAC_OFFSET; // MAC input from 67 to end
    } else {
        picc_data_offset = None;
        uid_offset = Some(find_value_offset("uid="));
        read_ctr_offset = Some(find_value_offset("ctr="));
        mac_offset = find_value_offset("cmac=");
        mac_input_offset = uri_body_file_offset;
    }

    SdmTemplateResult {
        ndef_record_hex: hex::encode(&ndef_record),
        ndef_file_hex: hex::encode(&ndef_file),
        nlen,
        ndef_file_len: ndef_file.len(),
        url_preview,
        uri_prefix_code: prefix_code,
        uri_body,
        picc_data_offset,
        uid_offset,
        read_ctr_offset,
        mac_input_offset,
        mac_offset,
    }
}

/// Build the SDM configuration to pass to ChangeFileSettings from a template result.
/// Uses declarative NdefAccessPolicy: public read, write/change require key 0.
/// When `nxp_compat` is true, overrides access_rights/SDM refs to match NXP AN12196 example
/// (debug 919E: chip may reject F0E0 or SDMCtrRet=0x0F during initial enrollment).
/// When `force_nxp_offsets` is true, overrides picc_data/mac offsets to 32, 67, 67.
/// The template now produces NXP-valid offsets by default; force_nxp_offsets is for debug override.
pub fn build_enrollment_sdm_config(
    template: &SdmTemplateResult,
    meta_read_key_no: u8,
    file_read_key_no: u8,
    access_policy: Option<&ntag424::NdefAccessPolicy>,
    nxp_compat: bool,
    force_nxp_offsets: bool,
) -> ntag424::SdmConfig {
    let encrypted_picc = template.picc_data_offset.is_some();
    let sdm_options = 0xC1; // UID + ReadCtr mirror + ASCII encoding (bit 0 must be set for hex output)

    let (access_rights, mut sdm_meta_read_key, sdm_file_read_key, sdm_ctr_ret_key) = if nxp_compat {
        (0x00E0, 0x02, 0x01, 0x00) // NXP-style; SDMCtrRet=0: auth with key 0 (admin path)
    } else {
        let default_policy = ntag424::NdefAccessPolicy::default();
        let policy = access_policy.unwrap_or(&default_policy);
        (
            policy.to_access_rights(),
            meta_read_key_no,
            file_read_key_no,
            0x00, // SDMCtrRet=0: auth with key 0 (admin path); GetFileCounters requires auth
        )
    };

    // NXP datasheet Table 69: SDMMetaRead = Eh selects plain PICCData mirroring
    // (UID + ReadCtr as separate plaintext fields). Values 0h..4h select encrypted mode.
    if !encrypted_picc {
        sdm_meta_read_key = 0x0E;
    }

    let (picc_data_offset, mac_input_offset, mac_offset) = if force_nxp_offsets {
        if encrypted_picc {
            (Some(32), Some(67), Some(67)) // NXP AN12196 offsets
        } else {
            // Plaintext mode uses UID/CTR mirrors, not encrypted PICCData.
            (None, Some(template.mac_input_offset), Some(template.mac_offset))
        }
    } else {
        (
            template.picc_data_offset,
            Some(template.mac_input_offset),
            Some(template.mac_offset),
        )
    };

    ntag424::SdmConfig {
        comm_mode: ntag424::CommMode::Plain,
        access_rights,
        sdm_options,
        sdm_meta_read_key,
        sdm_file_read_key,
        sdm_ctr_ret_key,
        uid_offset: template.uid_offset,
        read_ctr_offset: template.read_ctr_offset,
        read_ctr_limit: None,
        picc_data_offset,
        mac_input_offset,
        enc_offset: None,
        enc_length: None,
        mac_offset,
    }
}

/// Prepare the key change data for rotating an application key (01..04).
/// The old key is the transport key (all zeros) for first enrollment.
/// `key_version`: new key version to store on chip (0–255).
pub fn prepare_key_rotation(
    key_no: u8,
    new_key: &[u8; 16],
    old_key: &[u8; 16],
    key_version: u8,
    session: &Ev2Session,
) -> Vec<u8> {
    let _ = session; // session used by caller for encrypt+MAC
    if key_no == session.auth_key_no {
        ntag424::build_change_key_data_self(new_key, key_no, key_version)
    } else {
        ntag424::build_change_key_data_other(new_key, old_key, key_no, key_version)
    }
}

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

    #[test]
    fn url_template_offsets_encrypted_picc() {
        let policy = SdmTemplatePolicy { encrypted_picc: true, placeholder_char: '0' };
        let result = build_sdm_template("https://charm.farm/tap", &policy, None, None, None, None);
        assert!(result.url_preview.contains("charm.farm/tap"));
        assert!(result.url_preview.contains("?picc_data="));
        assert!(result.url_preview.contains("&c="));
        assert_eq!(result.uri_prefix_code, 0x04);
        assert!(result.picc_data_offset.is_some());
        assert!(result.uid_offset.is_none());
        assert_eq!(result.picc_data_offset.unwrap(), 32, "NXP-valid picc_data offset");
        assert_eq!(result.mac_offset, 67, "NXP-valid mac offset");
        assert_eq!(result.mac_input_offset, 67, "NXP-valid mac_input offset");
        assert!(result.ndef_file_len <= 256, "must fit NTAG 424 DNA NDEF file");
    }

    #[test]
    fn url_template_offsets_plaintext_mode() {
        let policy = SdmTemplatePolicy { encrypted_picc: false, placeholder_char: '0' };
        let result = build_sdm_template("https://charm.farm/tap", &policy, None, None, None, None);
        assert!(result.url_preview.contains("uid="));
        assert!(result.url_preview.contains("ctr="));
        assert!(result.picc_data_offset.is_none());
        assert!(result.uid_offset.is_some());
        assert!(result.read_ctr_offset.is_some());
    }

    #[test]
    fn enrollment_sdm_config_force_nxp_offsets_preserves_plaintext_layout() {
        let policy = SdmTemplatePolicy { encrypted_picc: false, placeholder_char: '0' };
        let template = build_sdm_template("https://charm.farm/tap", &policy, None, None, None, None);
        let cfg = build_enrollment_sdm_config(&template, 0x01, 0x02, None, true, true);

        assert_eq!(cfg.sdm_options, 0xC1, "ASCII bit must be set");
        assert_eq!(cfg.sdm_meta_read_key, 0x0E, "plaintext mode = SDMMetaRead Eh");
        assert_eq!(cfg.picc_data_offset, None);
        assert_eq!(cfg.uid_offset, template.uid_offset);
        assert_eq!(cfg.read_ctr_offset, template.read_ctr_offset);
        assert_eq!(cfg.mac_input_offset, Some(template.mac_input_offset));
        assert_eq!(cfg.mac_offset, Some(template.mac_offset));
    }

    #[test]
    fn plaintext_cmd_data_matches_expected_bytes() {
        let policy = SdmTemplatePolicy { encrypted_picc: false, placeholder_char: '0' };
        let template = build_sdm_template("https://charm.farm/tap", &policy, None, None, None, None);
        let cfg = build_enrollment_sdm_config(&template, 0x01, 0x02, None, true, true);
        let cmd_data = ntag424::build_sdm_file_settings(&cfg);
        let hex = hex::encode(&cmd_data);

        // Byte-by-byte per NXP datasheet Table 69 for plaintext SDM:
        //   40       FileOption (SDM enabled, CommMode Plain)
        //   00 E0    AccessRights (Read=free, Write=key0)
        //   C1       SDMOptions (UID + ReadCtr + ASCII)
        //   F0       SDMAccessRights[0] (RFU=F, SDMCtrRet=0)
        //   E1       SDMAccessRights[1] (SDMMetaRead=E plain, SDMFileRead=1)
        //   1A 00 00 UIDOffset = 26
        //   2D 00 00 SDMReadCtrOffset = 45
        //   07 00 00 SDMMACInputOffset = 7  (datasheet: before MACOffset)
        //   39 00 00 SDMMACOffset = 57      (datasheet: after MACInputOffset)
        assert_eq!(hex, "4000e0c1f0e11a00002d0000070000390000",
            "plaintext CmdData must match expected 18-byte layout");
        assert_eq!(cmd_data.len(), 18);
    }

    #[test]
    fn encrypted_picc_cmd_data_unchanged_by_offset_reorder() {
        let policy = SdmTemplatePolicy { encrypted_picc: true, placeholder_char: '0' };
        let template = build_sdm_template("https://charm.farm/tap", &policy, None, None, None, None);
        let cfg = build_enrollment_sdm_config(&template, 0x01, 0x02, None, true, true);
        let cmd_data = ntag424::build_sdm_file_settings(&cfg);
        let hex = hex::encode(&cmd_data);

        // NXP AN12196 example: both MAC offsets = 67, so order swap is a no-op.
        assert_eq!(hex, "4000e0c1f021200000430000430000",
            "encrypted PICC CmdData must match NXP AN12196 example");
        assert_eq!(cmd_data.len(), 15);
    }

    #[test]
    fn url_template_includes_art_id_and_instance_id_when_provided() {
        let policy = SdmTemplatePolicy::default();
        let result = build_sdm_template(
            "https://charm.farm/tap",
            &policy,
            Some("bafybeicbjdujzzxi5jcse2z5ugqbqyqpedxgjd3b7m4ceks7qlswq73kye"), // Base32 CID
            Some("xmp:iid:a6d17f93-18e8-4ad2-b601-7ab53382d1f2"),
            None, // claim_hash
            Some("abcdef0123456789"),
        );
        assert!(result.url_preview.contains("&art_id=bafybei"));
        assert!(result.url_preview.contains("&iid=a6d17f93-18e8-4ad2-b601-7ab53382d1f2"), "xmp:iid: prefix stripped, only UUID stored");
        assert!(result.url_preview.contains("&phash=abcdef0123456789"));
        assert!(result.ndef_file_len <= 256, "NDEF must fit in 256-byte File 02");
    }

    #[test]
    fn url_template_localhost_no_prefix() {
        // Plaintext mode: no padding, URL used as-is
        let policy = SdmTemplatePolicy { encrypted_picc: false, placeholder_char: '0' };
        let result = build_sdm_template("localhost:3000/tap", &policy, None, None, None, None);
        assert_eq!(result.uri_prefix_code, 0x00);
        assert!(result.url_preview.starts_with("localhost:3000/tap"));
    }

}
