#[derive(Clone, Debug)]
pub struct RefUpdate {
    pub name: String,
    pub old_oid: String,
    pub new_oid: String,
}

impl RefUpdate {
    pub fn parse_ref_updates(data: &[u8]) -> Result<Vec<Self>, String> {
        let mut refs = Vec::new();

        for payload in parse_pkt_line_payloads(data)? {
            let line = String::from_utf8_lossy(payload);
            let line = line.trim_end_matches(['\r', '\n']);
            if line.is_empty() {
                continue;
            }

            let mut parts = line.splitn(3, ' ');
            let old_oid = parts.next().unwrap_or_default();
            let new_oid = parts.next().unwrap_or_default();
            let raw_name = parts.next().unwrap_or_default();
            let name = raw_name
                .split_once('\0')
                .map(|(name, _)| name)
                .unwrap_or(raw_name)
                .trim();

            if old_oid.len() != 40 || new_oid.len() != 40 || name.is_empty() {
                continue;
            }

            refs.push(RefUpdate {
                old_oid: old_oid.to_string(),
                new_oid: new_oid.to_string(),
                name: name.to_string(),
            });
        }

        Ok(refs)
    }
}

fn parse_pkt_line_payloads(data: &[u8]) -> Result<Vec<&[u8]>, String> {
    let mut payloads = Vec::new();
    let mut offset = 0;

    while offset + 4 <= data.len() {
        let header = std::str::from_utf8(&data[offset..offset + 4])
            .map_err(|_| "invalid pkt-line header encoding".to_string())?;
        let len = usize::from_str_radix(header, 16)
            .map_err(|_| format!("invalid pkt-line length: {header}"))?;
        offset += 4;

        match len {
            0 => break,
            1..=3 => return Err(format!("invalid pkt-line length: {len}")),
            _ => {
                let payload_len = len - 4;
                if offset + payload_len > data.len() {
                    return Err("truncated pkt-line payload".to_string());
                }
                payloads.push(&data[offset..offset + payload_len]);
                offset += payload_len;
            }
        }
    }

    Ok(payloads)
}

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

    fn pkt(payload: &str) -> Vec<u8> {
        let len = payload.len() + 4;
        let mut out = format!("{len:04x}").into_bytes();
        out.extend_from_slice(payload.as_bytes());
        out
    }

    #[test]
    fn parses_receive_pack_ref_with_capabilities() {
        let mut data = pkt(
            "0000000000000000000000000000000000000000 1111111111111111111111111111111111111111 refs/heads/feature\0 report-status\n",
        );
        data.extend_from_slice(b"0000");

        let refs = RefUpdate::parse_ref_updates(&data).unwrap();

        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].old_oid, "0000000000000000000000000000000000000000");
        assert_eq!(refs[0].new_oid, "1111111111111111111111111111111111111111");
        assert_eq!(refs[0].name, "refs/heads/feature");
    }

    #[test]
    fn parses_receive_pack_ref_without_pack_payload() {
        let mut data = pkt(
            "2222222222222222222222222222222222222222 0000000000000000000000000000000000000000 refs/heads/old\n",
        );
        data.extend_from_slice(b"0000");

        let refs = RefUpdate::parse_ref_updates(&data).unwrap();

        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].name, "refs/heads/old");
        assert_eq!(refs[0].new_oid, "0000000000000000000000000000000000000000");
    }
}
