// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use std::fmt::{Display, Formatter};

use crate::Error;

const CURRENT_VERSION: u8 = 2;
const MAGIC: u64 = 0x424C4F4244455343; // "BLOBDESC"

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlobDescriptor {
    version: u8,
    uri: String,
    offset: i64,
    length: i64,
}

impl Display for BlobDescriptor {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "BlobDescriptor{{version={}, uri='{}', offset={}, length={}}}",
            self.version, self.uri, self.offset, self.length
        )
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BlobRangeSpec {
    offset: u64,
    length: Option<u64>,
}

impl BlobRangeSpec {
    pub(crate) fn offset(self) -> u64 {
        self.offset
    }

    pub(crate) fn length(self) -> Option<u64> {
        self.length
    }
}

impl BlobDescriptor {
    pub fn new(uri: String, offset: i64, length: i64) -> Self {
        Self {
            version: CURRENT_VERSION,
            uri,
            offset,
            length,
        }
    }

    pub fn uri(&self) -> &str {
        &self.uri
    }

    pub fn offset(&self) -> i64 {
        self.offset
    }

    pub fn length(&self) -> i64 {
        self.length
    }

    pub(crate) fn range_spec(&self) -> crate::Result<BlobRangeSpec> {
        if self.offset < 0 {
            return Err(Error::DataInvalid {
                message: format!(
                    "BlobDescriptor offset must be non-negative: {}",
                    self.offset
                ),
                source: None,
            });
        }
        if self.length < -1 {
            return Err(Error::DataInvalid {
                message: format!(
                    "BlobDescriptor length must be -1 or non-negative: {}",
                    self.length
                ),
                source: None,
            });
        }

        let offset = self.offset as u64;
        let length = if self.length == -1 {
            None
        } else {
            Some(self.length as u64)
        };
        if let Some(length) = length {
            offset
                .checked_add(length)
                .ok_or_else(|| Error::DataInvalid {
                    message: format!(
                        "BlobDescriptor range overflows u64: offset={offset}, length={length}"
                    ),
                    source: None,
                })?;
        }

        Ok(BlobRangeSpec { offset, length })
    }

    pub fn serialize(&self) -> Vec<u8> {
        let uri_bytes = self.uri.as_bytes();
        let uri_length = uri_bytes.len();
        let total_size = 1 + 8 + 4 + uri_length + 8 + 8;
        let mut buf = Vec::with_capacity(total_size);

        buf.push(self.version);
        buf.extend_from_slice(&MAGIC.to_le_bytes());
        buf.extend_from_slice(&(uri_length as i32).to_le_bytes());
        buf.extend_from_slice(uri_bytes);
        buf.extend_from_slice(&self.offset.to_le_bytes());
        buf.extend_from_slice(&self.length.to_le_bytes());

        buf
    }

    pub fn deserialize(bytes: &[u8]) -> crate::Result<Self> {
        if bytes.len() < 1 + 8 + 4 {
            return Err(Error::DataInvalid {
                message: "BlobDescriptor bytes too short".to_string(),
                source: None,
            });
        }

        let version = bytes[0];
        if version > CURRENT_VERSION {
            return Err(Error::Unsupported {
                message: format!(
                    "Expecting BlobDescriptor version <= {CURRENT_VERSION}, but found {version}"
                ),
            });
        }

        let mut pos = 1;

        if version > 1 {
            let magic = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
            if magic != MAGIC {
                return Err(Error::DataInvalid {
                    message: format!(
                        "Invalid BlobDescriptor: missing magic header. Expected {MAGIC:#X}, found {magic:#X}"
                    ),
                    source: None,
                });
            }
            pos += 8;
        }

        if bytes.len() < pos + 4 {
            return Err(Error::DataInvalid {
                message: "BlobDescriptor bytes too short for uri_length".to_string(),
                source: None,
            });
        }
        let uri_length_raw = i32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
        if uri_length_raw < 0 {
            return Err(Error::DataInvalid {
                message: format!("BlobDescriptor has negative uri_length: {uri_length_raw}"),
                source: None,
            });
        }
        let uri_length = uri_length_raw as usize;
        pos += 4;

        if bytes.len() < pos + uri_length + 16 {
            return Err(Error::DataInvalid {
                message: "BlobDescriptor bytes too short for uri + offset + length".to_string(),
                source: None,
            });
        }
        let uri = String::from_utf8(bytes[pos..pos + uri_length].to_vec()).map_err(|e| {
            Error::DataInvalid {
                message: format!("Invalid UTF-8 in BlobDescriptor uri: {e}"),
                source: Some(Box::new(e)),
            }
        })?;
        pos += uri_length;

        let offset = i64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
        pos += 8;
        let length = i64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());

        Ok(Self {
            version,
            uri,
            offset,
            length,
        })
    }

    pub fn is_blob_descriptor(bytes: &[u8]) -> bool {
        if bytes.len() < 9 {
            return false;
        }
        let version = bytes[0];
        if version > CURRENT_VERSION {
            return false;
        }
        let magic = u64::from_le_bytes(bytes[1..9].try_into().unwrap());
        magic == MAGIC
    }
}

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

    #[test]
    fn test_serialize_deserialize_roundtrip() {
        let desc = BlobDescriptor::new("s3://bucket/path/to/blob.blob".to_string(), 100, 2048);
        let bytes = desc.serialize();
        let deserialized = BlobDescriptor::deserialize(&bytes).unwrap();
        assert_eq!(desc, deserialized);
    }

    #[test]
    fn test_display_matches_java() {
        let desc = BlobDescriptor::new("file:///tmp/a".to_string(), 0, -1);
        assert_eq!(
            desc.to_string(),
            "BlobDescriptor{version=2, uri='file:///tmp/a', offset=0, length=-1}"
        );
    }

    #[test]
    fn test_is_blob_descriptor() {
        let desc = BlobDescriptor::new("file:///tmp/test.blob".to_string(), 0, 1024);
        let bytes = desc.serialize();
        assert!(BlobDescriptor::is_blob_descriptor(&bytes));
        assert!(!BlobDescriptor::is_blob_descriptor(&[0u8; 5]));
        assert!(!BlobDescriptor::is_blob_descriptor(b"not a descriptor"));
    }

    #[test]
    fn test_deserialize_rejects_future_version() {
        let mut bytes = BlobDescriptor::new("x".to_string(), 0, 0).serialize();
        bytes[0] = 255;
        assert!(BlobDescriptor::deserialize(&bytes).is_err());
    }

    #[test]
    fn test_deserialize_rejects_negative_uri_length() {
        let mut bytes = BlobDescriptor::new("x".to_string(), 0, 0).serialize();
        // Overwrite uri_length (at offset 9, after version + magic) with -1
        bytes[9..13].copy_from_slice(&(-1_i32).to_le_bytes());
        let err = BlobDescriptor::deserialize(&bytes).unwrap_err();
        assert!(
            matches!(err, Error::DataInvalid { message, .. } if message.contains("negative uri_length"))
        );
    }

    #[test]
    fn test_deserialize_rejects_bad_magic() {
        let mut bytes = BlobDescriptor::new("x".to_string(), 0, 0).serialize();
        bytes[1] = 0xFF;
        assert!(BlobDescriptor::deserialize(&bytes).is_err());
    }

    #[test]
    fn test_range_spec_supports_unknown_length() {
        let full = BlobDescriptor::new("x".to_string(), 0, -1)
            .range_spec()
            .unwrap();
        assert_eq!(full.offset(), 0);
        assert_eq!(full.length(), None);

        let range = BlobDescriptor::new("x".to_string(), 7, -1)
            .range_spec()
            .unwrap();
        assert_eq!(range.offset(), 7);
        assert_eq!(range.length(), None);
    }

    #[test]
    fn test_range_spec_supports_bounded_and_empty_ranges() {
        let bounded = BlobDescriptor::new("x".to_string(), 7, 11)
            .range_spec()
            .unwrap();
        assert_eq!(bounded.offset(), 7);
        assert_eq!(bounded.length(), Some(11));

        let empty = BlobDescriptor::new("x".to_string(), 7, 0)
            .range_spec()
            .unwrap();
        assert_eq!(empty.offset(), 7);
        assert_eq!(empty.length(), Some(0));
    }

    #[test]
    fn test_range_spec_rejects_invalid_signed_values() {
        let err = BlobDescriptor::new("x".to_string(), -1, 1)
            .range_spec()
            .unwrap_err();
        assert!(
            matches!(err, Error::DataInvalid { message, .. } if message.contains("offset must be non-negative"))
        );

        let err = BlobDescriptor::new("x".to_string(), 0, -2)
            .range_spec()
            .unwrap_err();
        assert!(
            matches!(err, Error::DataInvalid { message, .. } if message.contains("length must be -1 or non-negative"))
        );
    }

    #[test]
    fn test_range_spec_handles_largest_bounded_range() {
        let range = BlobDescriptor::new("x".to_string(), i64::MAX, i64::MAX)
            .range_spec()
            .unwrap();
        assert_eq!(range.offset(), i64::MAX as u64);
        assert_eq!(range.length(), Some(i64::MAX as u64));
    }
}
