use std::path::Path;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapPublicationPoint {
    pub header: SapPointHeader,
    pub manifest: Option<SapStoredManifest>,
    pub objects: Vec<SapStoredObject>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapPointHeader {
    pub version: u8,
    pub manifest_uri: String,
    pub rpki_notify: Option<String>,
    pub update_status: SapUpdateStatus,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SapUpdateStatus {
    Success { time_utc: time::OffsetDateTime },
    LastAttempt { time_utc: time::OffsetDateTime },
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapStoredManifest {
    pub not_after_utc: time::OffsetDateTime,
    pub manifest_number: [u8; 20],
    pub this_update_utc: time::OffsetDateTime,
    pub ca_repository: String,
    pub manifest_der: Vec<u8>,
    pub crl_uri: String,
    pub crl_der: Vec<u8>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SapStoredObject {
    pub uri: String,
    pub hash_sha256: Option<[u8; 32]>,
    pub content_der: Vec<u8>,
}

#[derive(Debug, thiserror::Error)]
pub enum SapDecodeError {
    #[error("I/O error reading {path}: {err}")]
    Io { path: String, err: String },

    #[error("SAP decode error: {0}")]
    Decode(String),
}

impl SapPublicationPoint {
    pub fn decode_path(path: &Path) -> Result<Self, SapDecodeError> {
        let bytes = std::fs::read(path).map_err(|e| SapDecodeError::Io {
            path: path.display().to_string(),
            err: e.to_string(),
        })?;
        Self::decode_bytes(&bytes)
    }

    pub fn decode_bytes(bytes: &[u8]) -> Result<Self, SapDecodeError> {
        let mut cur = Cursor::new(bytes);

        let version = cur.read_u8()?;
        let manifest_uri = cur.read_string_u32()?;
        let rpki_notify = cur.read_option_string_u32()?;

        let update_kind = cur.read_u8()?;
        let update_time_utc = cur.read_time_utc_i64_be()?;
        let update_status = match update_kind {
            0 => SapUpdateStatus::Success {
                time_utc: update_time_utc,
            },
            1 => SapUpdateStatus::LastAttempt {
                time_utc: update_time_utc,
            },
            _ => {
                return Err(SapDecodeError::Decode(format!(
                    "invalid update_status kind: {update_kind}"
                )));
            }
        };

        let header = SapPointHeader {
            version,
            manifest_uri,
            rpki_notify,
            update_status: update_status.clone(),
        };

        match update_status {
            SapUpdateStatus::LastAttempt { .. } => Ok(SapPublicationPoint {
                header,
                manifest: None,
                objects: Vec::new(),
            }),
            SapUpdateStatus::Success { .. } => {
                let not_after_utc = cur.read_time_utc_i64_be()?;
                let manifest_number = cur.read_array_20()?;
                let this_update_utc = cur.read_time_utc_i64_be()?;
                let ca_repository = cur.read_string_u32()?;
                let manifest_der = cur.read_bytes_u64_vec()?;
                let crl_uri = cur.read_string_u32()?;
                let crl_der = cur.read_bytes_u64_vec()?;

                let manifest = SapStoredManifest {
                    not_after_utc,
                    manifest_number,
                    this_update_utc,
                    ca_repository,
                    manifest_der,
                    crl_uri,
                    crl_der,
                };

                let mut objects = Vec::new();
                while cur.remaining() > 0 {
                    let uri = match cur.try_read_string_u32() {
                        Ok(Some(v)) => v,
                        Ok(None) => break,
                        Err(e) => return Err(e),
                    };

                    let hash_type = cur.read_u8()?;
                    let hash_sha256 = match hash_type {
                        0 => None,
                        1 => Some(cur.read_array_32()?),
                        other => {
                            return Err(SapDecodeError::Decode(format!(
                                "unsupported hash_type {other} for stored object {uri}"
                            )));
                        }
                    };
                    let content_der = cur.read_bytes_u64_vec()?;
                    objects.push(SapStoredObject {
                        uri,
                        hash_sha256,
                        content_der,
                    });
                }

                Ok(SapPublicationPoint {
                    header,
                    manifest: Some(manifest),
                    objects,
                })
            }
        }
    }
}

struct Cursor<'a> {
    bytes: &'a [u8],
    pos: usize,
}

impl<'a> Cursor<'a> {
    fn new(bytes: &'a [u8]) -> Self {
        Self { bytes, pos: 0 }
    }

    fn remaining(&self) -> usize {
        self.bytes.len().saturating_sub(self.pos)
    }

    fn read_exact(&mut self, len: usize) -> Result<&'a [u8], SapDecodeError> {
        if len > self.remaining() {
            return Err(SapDecodeError::Decode(format!(
                "truncated input: need {len} bytes, have {}",
                self.remaining()
            )));
        }
        let start = self.pos;
        self.pos += len;
        Ok(&self.bytes[start..start + len])
    }

    fn read_u8(&mut self) -> Result<u8, SapDecodeError> {
        Ok(self.read_exact(1)?[0])
    }

    fn read_u32_be(&mut self) -> Result<u32, SapDecodeError> {
        let b = self.read_exact(4)?;
        Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
    }

    fn read_u64_be(&mut self) -> Result<u64, SapDecodeError> {
        let b = self.read_exact(8)?;
        Ok(u64::from_be_bytes([
            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
        ]))
    }

    fn read_i64_be(&mut self) -> Result<i64, SapDecodeError> {
        let b = self.read_exact(8)?;
        Ok(i64::from_be_bytes([
            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
        ]))
    }

    fn read_time_utc_i64_be(&mut self) -> Result<time::OffsetDateTime, SapDecodeError> {
        let ts = self.read_i64_be()?;
        time::OffsetDateTime::from_unix_timestamp(ts)
            .map_err(|e| SapDecodeError::Decode(format!("invalid unix timestamp {ts}: {e}")))
    }

    fn read_string_u32(&mut self) -> Result<String, SapDecodeError> {
        let len = self.read_u32_be()? as usize;
        let b = self.read_exact(len)?;
        std::str::from_utf8(b)
            .map(|s| s.to_string())
            .map_err(|e| SapDecodeError::Decode(format!("invalid UTF-8 string: {e}")))
    }

    fn try_read_string_u32(&mut self) -> Result<Option<String>, SapDecodeError> {
        if self.remaining() == 0 {
            return Ok(None);
        }
        Ok(Some(self.read_string_u32()?))
    }

    fn read_option_string_u32(&mut self) -> Result<Option<String>, SapDecodeError> {
        let len = self.read_u32_be()? as usize;
        if len == 0 {
            return Ok(None);
        }
        let b = self.read_exact(len)?;
        let s = std::str::from_utf8(b)
            .map_err(|e| SapDecodeError::Decode(format!("invalid UTF-8 string: {e}")))?;
        Ok(Some(s.to_string()))
    }

    fn read_bytes_u64_vec(&mut self) -> Result<Vec<u8>, SapDecodeError> {
        let len_u64 = self.read_u64_be()?;
        let len = usize::try_from(len_u64).map_err(|_| {
            SapDecodeError::Decode(format!("data block too large for this system: {len_u64}"))
        })?;
        Ok(self.read_exact(len)?.to_vec())
    }

    fn read_array_20(&mut self) -> Result<[u8; 20], SapDecodeError> {
        let b = self.read_exact(20)?;
        let mut out = [0u8; 20];
        out.copy_from_slice(b);
        Ok(out)
    }

    fn read_array_32(&mut self) -> Result<[u8; 32], SapDecodeError> {
        let b = self.read_exact(32)?;
        let mut out = [0u8; 32];
        out.copy_from_slice(b);
        Ok(out)
    }
}
