// 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 crate::config::FullTextIndexMetadata;
use crate::error::{FtIndexError, Result};
use crate::io::{ReadRequest, SeekRead, SeekWrite};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::File;
use std::io::Read;
use std::path::{Component, Path};

pub const FORMAT_MAGIC: &[u8; 8] = b"PFTIDX01";
pub const FORMAT_VERSION: u32 = 1;
const MAX_HEADER_BYTES: usize = 16 * 1024 * 1024;
const MAX_ARCHIVE_READ_BATCH_BYTES: usize = 64 * 1024 * 1024;
const MAX_ARCHIVE_READ_BATCH_RANGES: usize = 64;
const ARCHIVE_WRITE_BUFFER_BYTES: usize = 64 * 1024;

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArchiveFileEntry {
    pub name: String,
    pub offset: u64,
    pub length: u64,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct IndexHeader {
    pub metadata: FullTextIndexMetadata,
    pub files: Vec<ArchiveFileEntry>,
}

pub fn write_envelope<W: SeekWrite>(
    output: &mut W,
    header: &IndexHeader,
    files: &[(String, Vec<u8>)],
) -> Result<()> {
    write_envelope_header(output, header)?;
    for (_, data) in files {
        output.write_all(data)?;
    }
    output.flush()?;
    Ok(())
}

pub(crate) fn write_envelope_from_paths<W: SeekWrite>(
    output: &mut W,
    header: &IndexHeader,
    paths: &[impl AsRef<Path>],
) -> Result<()> {
    if header.files.len() != paths.len() {
        return Err(FtIndexError::InvalidStorage(format!(
            "archive header contains {} files but {} paths were provided",
            header.files.len(),
            paths.len()
        )));
    }

    write_envelope_header(output, header)?;
    let mut buffer = vec![0u8; ARCHIVE_WRITE_BUFFER_BYTES];
    for (entry, path) in header.files.iter().zip(paths) {
        let mut file = File::open(path)?;
        let mut remaining = entry.length;
        while remaining > 0 {
            let chunk_len = usize::try_from(remaining.min(buffer.len() as u64))
                .expect("chunk length is bounded by the archive write buffer");
            file.read_exact(&mut buffer[..chunk_len])?;
            output.write_all(&buffer[..chunk_len])?;
            remaining -= chunk_len as u64;
        }
    }
    output.flush()?;
    Ok(())
}

fn write_envelope_header<W: SeekWrite>(output: &mut W, header: &IndexHeader) -> Result<()> {
    let header_json = serde_json::to_vec(header)?;
    if header_json.len() > MAX_HEADER_BYTES {
        return Err(FtIndexError::InvalidStorage(format!(
            "header too large: {} bytes exceeds {} bytes",
            header_json.len(),
            MAX_HEADER_BYTES
        )));
    }
    output.write_all(FORMAT_MAGIC)?;
    output.write_all(&FORMAT_VERSION.to_be_bytes())?;
    output.write_all(
        &u32::try_from(header_json.len())
            .map_err(|_| FtIndexError::InvalidStorage("header too large".to_string()))?
            .to_be_bytes(),
    )?;
    output.write_all(&header_json)?;
    Ok(())
}

pub fn read_header<R: SeekRead>(input: &R) -> Result<(IndexHeader, u64)> {
    let mut fixed = [0u8; 16];
    read_exact_at(input, 0, &mut fixed)?;
    if &fixed[0..8] != FORMAT_MAGIC {
        return Err(FtIndexError::InvalidStorage("bad magic".to_string()));
    }
    let version = u32::from_be_bytes(fixed[8..12].try_into().expect("slice length"));
    if version != FORMAT_VERSION {
        return Err(FtIndexError::InvalidStorage(format!(
            "unsupported format version {version}"
        )));
    }
    let header_len = u32::from_be_bytes(fixed[12..16].try_into().expect("slice length")) as usize;
    if header_len > MAX_HEADER_BYTES {
        return Err(FtIndexError::InvalidStorage(format!(
            "header too large: {header_len} bytes exceeds {MAX_HEADER_BYTES} bytes"
        )));
    }
    let mut header_json = vec![0u8; header_len];
    read_exact_at(input, 16, &mut header_json)?;
    let header = serde_json::from_slice(&header_json)?;
    validate_header(&header)?;
    Ok((header, 16 + header_len as u64))
}

fn validate_header(header: &IndexHeader) -> Result<()> {
    header.metadata.config.validate()?;
    if header.metadata.tantivy_version.trim().is_empty() {
        return Err(FtIndexError::InvalidStorage(
            "missing Tantivy version in index metadata".to_string(),
        ));
    }
    if header.files.is_empty() {
        return Err(FtIndexError::InvalidStorage(
            "archive file list must not be empty".to_string(),
        ));
    }

    let mut names = HashSet::with_capacity(header.files.len());
    let mut ranges = Vec::with_capacity(header.files.len());
    for file in &header.files {
        validate_archive_file_name(&file.name)?;
        if !names.insert(file.name.as_str()) {
            return Err(FtIndexError::InvalidStorage(format!(
                "duplicate archive file '{}'",
                file.name
            )));
        }
        let end = file.offset.checked_add(file.length).ok_or_else(|| {
            FtIndexError::InvalidStorage(format!("archive file '{}' range overflow", file.name))
        })?;
        ranges.push((file.offset, end, file.name.as_str()));
    }

    ranges.sort_by_key(|(start, _, _)| *start);
    let mut previous_end = 0u64;
    for (start, end, name) in ranges {
        if start < previous_end {
            return Err(FtIndexError::InvalidStorage(format!(
                "archive file '{name}' overlaps a previous file"
            )));
        }
        previous_end = end;
    }
    Ok(())
}

fn validate_archive_file_name(name: &str) -> Result<()> {
    if name.is_empty() {
        return Err(FtIndexError::InvalidStorage(
            "archive file name must not be empty".to_string(),
        ));
    }
    let mut components = Path::new(name).components();
    match (components.next(), components.next()) {
        (Some(Component::Normal(_)), None) => Ok(()),
        _ => Err(FtIndexError::InvalidStorage(format!(
            "archive file '{name}' must be a plain file name"
        ))),
    }
}

pub fn read_archive_files<R: SeekRead>(
    input: &R,
    body_start: u64,
    files: &[ArchiveFileEntry],
) -> Result<Vec<(String, Vec<u8>)>> {
    read_archive_files_with(input, body_start, files, |name, data| {
        Ok((name.to_string(), data.to_vec()))
    })
}

pub fn read_archive_files_with<R, F, T>(
    input: &R,
    body_start: u64,
    files: &[ArchiveFileEntry],
    mut consume: F,
) -> Result<Vec<T>>
where
    R: SeekRead,
    F: FnMut(&str, &[u8]) -> Result<T>,
{
    let mut output = Vec::with_capacity(files.len());
    let mut pending = Vec::new();
    let mut pending_bytes = 0usize;

    for file in files {
        let length = usize::try_from(file.length).map_err(|_| {
            FtIndexError::InvalidStorage(format!("archive file '{}' is too large", file.name))
        })?;
        let pos = body_start.checked_add(file.offset).ok_or_else(|| {
            FtIndexError::InvalidStorage(format!("archive file '{}' offset overflow", file.name))
        })?;

        if !pending.is_empty()
            && (pending.len() >= MAX_ARCHIVE_READ_BATCH_RANGES
                || pending_bytes.saturating_add(length) > MAX_ARCHIVE_READ_BATCH_BYTES)
        {
            read_archive_file_batch(input, &mut pending, &mut consume, &mut output)?;
            pending_bytes = 0;
        }

        pending.push(PendingArchiveFile {
            name: file.name.clone(),
            pos,
            data: vec![0u8; length],
        });
        pending_bytes = pending_bytes.saturating_add(length);
    }

    if !pending.is_empty() {
        read_archive_file_batch(input, &mut pending, &mut consume, &mut output)?;
    }

    Ok(output)
}

struct PendingArchiveFile {
    name: String,
    pos: u64,
    data: Vec<u8>,
}

fn read_archive_file_batch<R, F, T>(
    input: &R,
    pending: &mut Vec<PendingArchiveFile>,
    consume: &mut F,
    output: &mut Vec<T>,
) -> Result<()>
where
    R: SeekRead,
    F: FnMut(&str, &[u8]) -> Result<T>,
{
    {
        let mut requests: Vec<_> = pending
            .iter_mut()
            .map(|file| ReadRequest::new(file.pos, file.data.as_mut_slice()))
            .collect();
        input.pread(&mut requests)?;
    }

    for file in pending.drain(..) {
        output.push(consume(&file.name, &file.data)?);
    }
    Ok(())
}

pub fn read_exact_at<R: SeekRead>(input: &R, pos: u64, buf: &mut [u8]) -> Result<()> {
    let mut request = [ReadRequest::new(pos, buf)];
    input.pread(&mut request)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{FullTextIndexConfig, FullTextIndexMetadata};
    use crate::io::{PosWriter, SliceReader};
    use std::sync::atomic::{AtomicUsize, Ordering};

    struct CountingReader {
        data: Vec<u8>,
        pread_batches: AtomicUsize,
        max_ranges_per_batch: AtomicUsize,
    }

    impl SeekRead for CountingReader {
        fn pread(&self, ranges: &mut [ReadRequest<'_>]) -> std::io::Result<()> {
            self.pread_batches.fetch_add(1, Ordering::SeqCst);
            self.max_ranges_per_batch
                .fetch_max(ranges.len(), Ordering::SeqCst);
            for range in ranges {
                let start = usize::try_from(range.pos).map_err(|_| {
                    std::io::Error::new(std::io::ErrorKind::InvalidInput, "offset overflow")
                })?;
                let end = start.checked_add(range.buf.len()).ok_or_else(|| {
                    std::io::Error::new(std::io::ErrorKind::InvalidInput, "range overflow")
                })?;
                if end > self.data.len() {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::UnexpectedEof,
                        "read past end of slice",
                    ));
                }
                range.buf.copy_from_slice(&self.data[start..end]);
            }
            Ok(())
        }
    }

    #[test]
    fn read_archive_files_uses_one_batched_pread() {
        let reader = CountingReader {
            data: b"headerabcdef".to_vec(),
            pread_batches: AtomicUsize::new(0),
            max_ranges_per_batch: AtomicUsize::new(0),
        };
        let files = vec![
            ArchiveFileEntry {
                name: "a".to_string(),
                offset: 0,
                length: 2,
            },
            ArchiveFileEntry {
                name: "b".to_string(),
                offset: 2,
                length: 3,
            },
            ArchiveFileEntry {
                name: "c".to_string(),
                offset: 5,
                length: 1,
            },
        ];

        let files = read_archive_files(&reader, 6, &files).expect("archive files");

        assert_eq!(reader.pread_batches.load(Ordering::SeqCst), 1);
        assert_eq!(reader.max_ranges_per_batch.load(Ordering::SeqCst), 3);
        assert_eq!(
            files,
            vec![
                ("a".to_string(), b"ab".to_vec()),
                ("b".to_string(), b"cde".to_vec()),
                ("c".to_string(), b"f".to_vec()),
            ]
        );
    }

    #[test]
    fn read_header_rejects_invalid_archive_metadata() {
        let cases = [
            (
                vec![
                    ArchiveFileEntry {
                        name: "meta.json".to_string(),
                        offset: 0,
                        length: 2,
                    },
                    ArchiveFileEntry {
                        name: "meta.json".to_string(),
                        offset: 2,
                        length: 2,
                    },
                ],
                "duplicate archive file",
            ),
            (
                vec![ArchiveFileEntry {
                    name: "../meta.json".to_string(),
                    offset: 0,
                    length: 2,
                }],
                "must be a plain file name",
            ),
            (
                vec![
                    ArchiveFileEntry {
                        name: "a".to_string(),
                        offset: 0,
                        length: 3,
                    },
                    ArchiveFileEntry {
                        name: "b".to_string(),
                        offset: 2,
                        length: 3,
                    },
                ],
                "overlaps a previous file",
            ),
        ];

        for (files, expected) in cases {
            let bytes = encode_header(IndexHeader {
                metadata: valid_metadata(),
                files,
            });
            let err = read_header(&SliceReader::new(bytes)).expect_err("invalid header");
            assert!(
                err.to_string().contains(expected),
                "expected '{expected}', got '{err}'"
            );
        }
    }

    #[test]
    fn read_header_rejects_missing_tantivy_version() {
        let bytes = encode_header(IndexHeader {
            metadata: FullTextIndexMetadata {
                tantivy_version: String::new(),
                ..valid_metadata()
            },
            files: vec![ArchiveFileEntry {
                name: "meta.json".to_string(),
                offset: 0,
                length: 2,
            }],
        });

        let err = read_header(&SliceReader::new(bytes)).expect_err("invalid header");

        assert!(err
            .to_string()
            .contains("missing Tantivy version in index metadata"));
    }

    #[test]
    fn read_header_rejects_oversized_header_before_allocating() {
        const OVERSIZED_HEADER_LEN: u32 = 16 * 1024 * 1024 + 1;

        let mut bytes = Vec::new();
        bytes.extend_from_slice(FORMAT_MAGIC);
        bytes.extend_from_slice(&FORMAT_VERSION.to_be_bytes());
        bytes.extend_from_slice(&OVERSIZED_HEADER_LEN.to_be_bytes());

        let err = read_header(&SliceReader::new(bytes)).expect_err("oversized header should fail");

        assert!(err.to_string().contains("header too large"));
    }

    fn valid_metadata() -> FullTextIndexMetadata {
        FullTextIndexMetadata {
            config: FullTextIndexConfig::new(),
            document_count: 1,
            tantivy_version: tantivy::version().to_string(),
        }
    }

    fn encode_header(header: IndexHeader) -> Vec<u8> {
        let mut bytes = Vec::new();
        write_envelope(
            &mut PosWriter::new(&mut bytes),
            &header,
            &[("meta.json".to_string(), b"ab".to_vec())],
        )
        .expect("write envelope");
        bytes
    }
}
