// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2026 Petr Hodina <petr.hodina@protonmail.com>

//! QMI service-message framing for the SSC service (`sns_client_api_v01`),
//! following libssc's wire format.
//!
//! Header (7 bytes):
//!
//! ```text
//! u8 ctrl_flag | u16 LE tx_id | u16 LE msg_id | u16 LE body_len
//! ```
//!
//! TLV layouts depend on value type:
//!
//! - **INT** / **SEQUENCE**: `u8 type | u16 LE length | length bytes value`
//! - **ARRAY** (libssc-specific): `u8 type | u16 LE outer_len | u16 LE inner_len | value`
//!   where `outer_len = inner_len + 2`. The doubled length field is the
//!   non-standard part — Qualcomm's SSC service expects it for any
//!   length-prefixed array TLV.

use bytes::{Buf, BufMut, BytesMut};

use crate::error::{Error, Result};

pub const FLAG_REQ: u8 = 0x00;
pub const FLAG_RESP: u8 = 0x02;
pub const FLAG_IND: u8 = 0x04;

/// `sns_client_api_v01` message IDs from libssc.
pub const SSC_CONTROL_MSG_ID: u16 = 0x0020;
pub const SSC_REPORT_SMALL_MSG_ID: u16 = 0x0021;
pub const SSC_REPORT_LARGE_MSG_ID: u16 = 0x0022;

/// Request TLV IDs (control message input).
pub const TLV_REQUEST_DATA: u8 = 0x01; // ARRAY: protobuf payload
pub const TLV_REQUEST_REPORT_TYPE: u8 = 0x10; // INT: 0=small, 1=large

/// Response TLV IDs (control message output).
pub const TLV_RESPONSE_RESULT: u8 = 0x02; // SEQUENCE: 4 bytes (result + error)
pub const TLV_RESPONSE_CLIENT_ID: u8 = 0x10; // INT: 4 bytes assigned client_id

/// Indication TLV IDs (report).
pub const TLV_REPORT_CLIENT_ID: u8 = 0x01; // INT: 4 bytes (small) or 8 bytes (large)
pub const TLV_REPORT_DATA: u8 = 0x02; // ARRAY: protobuf SscClientResponse

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
    Int,
    Sequence,
    Array,
}

#[derive(Debug, Clone)]
pub struct Tlv {
    pub ty: u8,
    pub value: Vec<u8>,
    pub kind: ValueType,
}

#[derive(Debug, Clone)]
pub struct QmiMessage {
    pub flag: u8,
    pub tx_id: u16,
    pub msg_id: u16,
    pub tlvs: Vec<Tlv>,
}

impl QmiMessage {
    pub fn request(tx_id: u16, msg_id: u16) -> Self {
        Self { flag: FLAG_REQ, tx_id, msg_id, tlvs: Vec::new() }
    }

    pub fn add_tlv(mut self, ty: u8, value: Vec<u8>, kind: ValueType) -> Self {
        self.tlvs.push(Tlv { ty, value, kind });
        self
    }

    pub fn tlv(&self, ty: u8) -> Option<&Tlv> {
        self.tlvs.iter().find(|t| t.ty == ty)
    }

    pub fn encode(&self) -> Vec<u8> {
        // Compute body length first (each TLV's bytes on the wire).
        let body_len: usize = self.tlvs.iter().map(|t| 3 + tlv_value_wire_len(t)).sum();

        let mut buf = BytesMut::with_capacity(7 + body_len);
        buf.put_u8(self.flag);
        buf.put_u16_le(self.tx_id);
        buf.put_u16_le(self.msg_id);
        buf.put_u16_le(body_len as u16);
        for tlv in &self.tlvs {
            buf.put_u8(tlv.ty);
            buf.put_u16_le(tlv_value_wire_len(tlv) as u16);
            if tlv.kind == ValueType::Array {
                // libssc array TLV: extra inner length prefix.
                buf.put_u16_le(tlv.value.len() as u16);
            }
            buf.put_slice(&tlv.value);
        }
        buf.to_vec()
    }

    /// Decode a wire packet. The TLV value-type is unknown at decode time
    /// (the SSC service doesn't tag it on the wire), so all parsed TLVs are
    /// returned as raw bytes — interpretation per ID is the caller's job.
    pub fn decode(mut buf: &[u8]) -> Result<Self> {
        if buf.len() < 7 {
            return Err(Error::Qmi(format!("short header: {} bytes", buf.len())));
        }
        let flag = buf.get_u8();
        let tx_id = buf.get_u16_le();
        let msg_id = buf.get_u16_le();
        let body_len = buf.get_u16_le() as usize;
        if buf.len() < body_len {
            return Err(Error::Qmi(format!("truncated body: header says {body_len}, have {}", buf.len())));
        }
        let mut body = &buf[..body_len];
        let mut tlvs = Vec::new();
        while !body.is_empty() {
            if body.len() < 3 {
                return Err(Error::Qmi("truncated TLV header".into()));
            }
            let ty = body.get_u8();
            let len = body.get_u16_le() as usize;
            if body.len() < len {
                return Err(Error::Qmi(format!("truncated TLV value (need {len})")));
            }
            let (val, rest) = body.split_at(len);
            tlvs.push(Tlv {
                ty,
                value: val.to_vec(),
                kind: ValueType::Sequence, // unknown at decode time
            });
            body = rest;
        }
        Ok(Self { flag, tx_id, msg_id, tlvs })
    }
}

fn tlv_value_wire_len(t: &Tlv) -> usize {
    match t.kind {
        ValueType::Array => 2 + t.value.len(), // inner-length prefix
        _ => t.value.len(),
    }
}

/// Strip the libssc array TLV's inner length prefix from a received TLV
/// value. Used when the caller knows TLV `ty` is an ARRAY-shaped one (e.g.
/// `TLV_REQUEST_DATA` in indications).
pub fn array_payload(value: &[u8]) -> Result<&[u8]> {
    if value.len() < 2 {
        return Err(Error::Qmi("ARRAY TLV value missing inner length".into()));
    }
    let inner = u16::from_le_bytes([value[0], value[1]]) as usize;
    let body = &value[2..];
    if body.len() < inner {
        return Err(Error::Qmi(format!("ARRAY TLV inner length {inner} > body {}", body.len())));
    }
    Ok(&body[..inner])
}

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

    #[test]
    fn encode_decode_round_trip() {
        let msg = QmiMessage::request(0x1234, SSC_CONTROL_MSG_ID)
            .add_tlv(TLV_REQUEST_REPORT_TYPE, vec![1], ValueType::Int)
            .add_tlv(TLV_REQUEST_DATA, b"hello".to_vec(), ValueType::Array);

        let wire = msg.encode();
        let decoded = QmiMessage::decode(&wire).unwrap();

        assert_eq!(decoded.flag, FLAG_REQ);
        assert_eq!(decoded.tx_id, 0x1234);
        assert_eq!(decoded.msg_id, SSC_CONTROL_MSG_ID);

        // INT TLV is round-tripped verbatim.
        assert_eq!(decoded.tlv(TLV_REQUEST_REPORT_TYPE).unwrap().value, vec![1]);

        // ARRAY TLV needs array_payload to strip the libssc inner-length prefix.
        let array_value = &decoded.tlv(TLV_REQUEST_DATA).unwrap().value;
        assert_eq!(array_payload(array_value).unwrap(), b"hello");
    }

    #[test]
    fn encode_array_tlv_includes_inner_length_prefix() {
        // ARRAY TLV with a 4-byte payload. Wire layout:
        //   header(7) + ty(1) + outer_len(2) + inner_len(2) + payload(4) = 16
        let msg = QmiMessage::request(1, SSC_CONTROL_MSG_ID).add_tlv(TLV_REQUEST_DATA, vec![1, 2, 3, 4], ValueType::Array);
        let wire = msg.encode();
        assert_eq!(wire.len(), 16);
        // outer_len = inner_len + 2 (libssc-specific quirk)
        assert_eq!(&wire[8..10], &[0x06, 0x00]);
        // inner_len = 4
        assert_eq!(&wire[10..12], &[0x04, 0x00]);
        // payload
        assert_eq!(&wire[12..16], &[1, 2, 3, 4]);
    }

    #[test]
    fn encode_int_tlv_omits_inner_length_prefix() {
        // INT TLV has no inner-length prefix — its outer length matches the
        // raw value length exactly.
        let msg = QmiMessage::request(1, SSC_CONTROL_MSG_ID).add_tlv(TLV_REQUEST_REPORT_TYPE, vec![0xab], ValueType::Int);
        let wire = msg.encode();
        // header(7) + ty(1) + len(2) + value(1) = 11
        assert_eq!(wire.len(), 11);
        assert_eq!(&wire[8..10], &[0x01, 0x00]);
        assert_eq!(wire[10], 0xab);
    }

    #[test]
    fn decode_rejects_short_header() {
        // Anything under 7 bytes can't carry a complete header.
        assert!(QmiMessage::decode(&[]).is_err());
        assert!(QmiMessage::decode(&[0; 6]).is_err());
    }

    #[test]
    fn decode_rejects_truncated_body() {
        // Header advertises body_len=100 but no body bytes follow.
        let bad = [0x00, 0x01, 0x00, 0x20, 0x00, 0x64, 0x00];
        assert!(QmiMessage::decode(&bad).is_err());
    }

    #[test]
    fn decode_rejects_truncated_tlv() {
        // Header(7) + ty(1) + len=10(2) + value(2 bytes only) = 12.
        // body_len must match the bytes that follow, otherwise body_len
        // mismatch fires before the TLV check; here we stage a body that
        // matches the header but the TLV inside claims more than the body
        // contains.
        let mut buf = vec![0u8; 0];
        buf.extend_from_slice(&[0x00, 0x01, 0x00, 0x20, 0x00, 0x05, 0x00]); // header, body=5
        buf.extend_from_slice(&[0x10, 0x0a, 0x00, 0x01, 0x02]); // TLV ty=10, len=10, only 2 bytes
        assert!(QmiMessage::decode(&buf).is_err());
    }

    #[test]
    fn array_payload_strips_inner_length() {
        let raw = [0x05, 0x00, b'h', b'e', b'l', b'l', b'o'];
        assert_eq!(array_payload(&raw).unwrap(), b"hello");
    }

    #[test]
    fn array_payload_rejects_too_short() {
        assert!(array_payload(&[]).is_err());
        assert!(array_payload(&[0x01]).is_err());
    }

    #[test]
    fn array_payload_rejects_truncated_value() {
        // Inner length says 10, only 3 bytes follow it.
        let raw = [0x0a, 0x00, b'a', b'b', b'c'];
        assert!(array_payload(&raw).is_err());
    }
}
