use gix_object::bstr::{BStr, ByteSlice};

use crate::{parse, store_impl::packed};

#[derive(Debug, PartialEq, Eq)]
enum Peeled {
    Unspecified,
    Partial,
    Fully,
}

/// Information parsed from the header of a packed ref file
#[derive(Debug, PartialEq, Eq)]
pub struct Header {
    peeled: Peeled,
    pub sorted: bool,
}

impl Default for Header {
    fn default() -> Self {
        Header {
            peeled: Peeled::Unspecified,
            sorted: false,
        }
    }
}

/// Return the bytes before the next line ending as a [`BStr`].
///
/// On success, `input` is advanced past the line ending. The returned slice
/// does not include the line ending.
fn until_line_end_without_separator<'a>(input: &mut &'a [u8]) -> Result<&'a BStr, ()> {
    let line_end = input.iter().position(|b| *b == b'\r' || *b == b'\n').ok_or(())?;
    let out = input[..line_end].as_bstr();
    let mut maybe_start_of_newline = &input[line_end..];
    parse::newline(&mut maybe_start_of_newline)?;
    *input = maybe_start_of_newline;
    Ok(out)
}

/// Parse a `packed-refs` header line.
///
/// A valid header starts with `# pack-refs with: ` and ends with a line ending.
/// Known space-separated traits after the prefix populate the returned
/// [`Header`]: `peeled`, `fully-peeled`, and `sorted`. Unknown traits are
/// ignored.
///
/// On success, `input` is advanced past the entire header line, including its
/// line ending.
pub fn header(input: &mut &[u8]) -> Result<Header, ()> {
    let Some(rest) = input.strip_prefix(b"# pack-refs with: ") else {
        return Err(());
    };
    *input = rest;
    let traits = until_line_end_without_separator(input)?;
    let mut peeled = Peeled::Unspecified;
    let mut sorted = false;
    for token in traits.split_str(b" ") {
        if token == b"fully-peeled" {
            peeled = Peeled::Fully;
        } else if token == b"peeled" {
            peeled = Peeled::Partial;
        } else if token == b"sorted" {
            sorted = true;
        }
    }
    Ok(Header { peeled, sorted })
}

/// Parse one packed reference entry and its optional peeled object line.
///
/// The reference line has the form `<hex-object-id> <ref-name>` followed by a
/// line ending. If the following line starts with `^`, it is parsed as the
/// peeled object id for the returned [`packed::Reference`].
/// Object ids are parsed according to `object_hash`.
///
/// On success, `input` is advanced past the reference line and, if present, the
/// peeled object line.
pub fn reference<'a>(input: &mut &'a [u8], object_hash: gix_hash::Kind) -> Result<packed::Reference<'a>, ()> {
    let target = parse::hex_hash(input, object_hash)?;
    let Some(rest) = input.strip_prefix(b" ") else {
        return Err(());
    };
    *input = rest;
    let name = until_line_end_without_separator(input)?.try_into().map_err(|_| ())?;

    let object = if let Some(rest) = input.strip_prefix(b"^") {
        *input = rest;
        let object = parse::hex_hash(input, object_hash)?;
        parse::newline(input)?;
        Some(object)
    } else {
        None
    };

    Ok(packed::Reference { name, target, object })
}

/// Extract the name bytes from a packed-refs record without validating the
/// name as a [`FullNameRef`][crate::FullNameRef] or the hash as hex.
///
/// Used by the binary search in [`super::Buffer::binary_search_by`], where
/// only the name bytes are needed for ordered comparison — the final match
/// is re-parsed through [`reference`] which validates fully.
///
/// Returns `None` when the record does not have the expected
/// `<hash><space><name><newline>` shape (including an empty name or input
/// shorter than the hash plus separator).
pub(crate) fn name_at_record_start(input: &[u8], object_hash: gix_hash::Kind) -> Option<&[u8]> {
    let hex_len = object_hash.len_in_hex();
    if input.get(hex_len) != Some(&b' ') {
        return None;
    }
    let after = &input[hex_len + 1..];
    let end = after.iter().position(|&b| b == b'\r' || b == b'\n')?;
    if end == 0 {
        return None;
    }
    if after[end] == b'\r' && after.get(end + 1) != Some(&b'\n') {
        return None;
    }
    Some(&after[..end])
}

/// Return the byte offset of the packed-refs record containing `offset`.
///
/// If `offset` points into a peeled object line, the returned offset points to
/// the owning reference record instead.
pub(crate) fn record_start_at_offset(input: &[u8], offset: usize) -> usize {
    input[..offset]
        .rfind(b"\n")
        .and_then(|pos| {
            let candidate = pos + 1;
            let b = input.get(candidate)?;
            if *b == b'^' {
                input[..pos].rfind(b"\n").map(|pos| pos + 1)
            } else {
                Some(candidate)
            }
        })
        .unwrap_or(0)
}

/// Return the packed-refs record containing `offset`.
///
/// The returned slice begins at the owning reference line. It includes the
/// remainder of `input`, which is enough for callers that only need to parse
/// the first record.
pub(crate) fn record_at_offset(input: &[u8], offset: usize) -> &[u8] {
    &input[record_start_at_offset(input, offset)..]
}

#[cfg(test)]
mod tests;
