use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};

use xxhash_rust::xxh3::Xxh3;

use crate::error::{Error, Result};

/// A streaming chunk read from a file.
#[derive(Debug)]
pub struct StreamChunk {
    /// The chunk content.
    pub data: Vec<u8>,

    /// Byte offset in the file.
    pub offset: u64,

    /// Whether this is the last chunk.
    pub is_last: bool,

    /// Incremental hash up to and including this chunk.
    pub cumulative_hash: u64,
}

impl StreamChunk {
    /// Try to convert the chunk data to a UTF-8 string.
    pub fn as_str(&self) -> Option<&str> {
        std::str::from_utf8(&self.data).ok()
    }

    /// Convert to string, replacing invalid UTF-8 with replacement character.
    pub fn to_string_lossy(&self) -> String {
        String::from_utf8_lossy(&self.data).into_owned()
    }
}

/// Iterator that streams file content in chunks.
pub struct ContentStream {
    reader: BufReader<File>,
    path: PathBuf,
    chunk_size: usize,
    offset: u64,
    file_size: u64,
    hasher: Xxh3,
}

impl ContentStream {
    /// Create a new content stream for a file.
    ///
    /// # Arguments
    /// * `path` - Path to the file to stream
    /// * `chunk_size` - Size of each chunk in bytes
    pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        let file = File::open(&path).map_err(|e| Error::Io {
            path: path.clone(),
            source: e,
        })?;

        let file_size = file
            .metadata()
            .map_err(|e| Error::Io {
                path: path.clone(),
                source: e,
            })?
            .len();

        Ok(Self {
            reader: BufReader::new(file),
            path,
            chunk_size,
            offset: 0,
            file_size,
            hasher: Xxh3::new(),
        })
    }

    /// Get the file size.
    pub fn file_size(&self) -> u64 {
        self.file_size
    }

    /// Get the current offset.
    pub fn offset(&self) -> u64 {
        self.offset
    }

    /// Get the remaining bytes.
    pub fn remaining(&self) -> u64 {
        self.file_size.saturating_sub(self.offset)
    }

    /// Check if the stream is finished.
    pub fn is_finished(&self) -> bool {
        self.offset >= self.file_size
    }

    /// Read the next chunk.
    pub fn next_chunk(&mut self) -> Result<Option<StreamChunk>> {
        if self.is_finished() {
            return Ok(None);
        }

        let mut buffer = vec![0u8; self.chunk_size.min(self.remaining() as usize)];
        let bytes_read = self.reader.read(&mut buffer).map_err(|e| Error::Io {
            path: self.path.clone(),
            source: e,
        })?;

        if bytes_read == 0 {
            return Ok(None);
        }

        buffer.truncate(bytes_read);
        self.hasher.update(&buffer);

        let chunk_offset = self.offset;
        self.offset += bytes_read as u64;

        Ok(Some(StreamChunk {
            data: buffer,
            offset: chunk_offset,
            is_last: self.is_finished(),
            cumulative_hash: self.hasher.digest(),
        }))
    }

    /// Reset the stream to the beginning.
    pub fn reset(&mut self) -> Result<()> {
        self.reader
            .seek(SeekFrom::Start(0))
            .map_err(|e| Error::Io {
                path: self.path.clone(),
                source: e,
            })?;
        self.offset = 0;
        self.hasher.reset();
        Ok(())
    }
}

impl Iterator for ContentStream {
    type Item = Result<StreamChunk>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.next_chunk() {
            Ok(Some(chunk)) => Some(Ok(chunk)),
            Ok(None) => None,
            Err(e) => Some(Err(e)),
        }
    }
}

/// Stream file content line by line.
pub struct LineStream {
    reader: BufReader<File>,
    path: PathBuf,
    line_number: usize,
    offset: u64,
}

impl LineStream {
    /// Create a new line stream.
    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        let file = File::open(&path).map_err(|e| Error::Io {
            path: path.clone(),
            source: e,
        })?;

        Ok(Self {
            reader: BufReader::new(file),
            path,
            line_number: 0,
            offset: 0,
        })
    }

    /// Get the current line number (1-indexed after first read).
    pub fn line_number(&self) -> usize {
        self.line_number
    }

    /// Get the current byte offset.
    pub fn offset(&self) -> u64 {
        self.offset
    }
}

/// A single line from a file.
#[derive(Debug, Clone)]
pub struct FileLine {
    /// The line content (without trailing newline).
    pub content: String,

    /// Line number (1-indexed).
    pub line_number: usize,

    /// Byte offset of the line start.
    pub offset: u64,
}

impl Iterator for LineStream {
    type Item = Result<FileLine>;

    fn next(&mut self) -> Option<Self::Item> {
        let mut line = String::new();
        let line_offset = self.offset;

        match self.reader.read_line(&mut line) {
            Ok(0) => None,
            Ok(bytes_read) => {
                self.offset += bytes_read as u64;
                self.line_number += 1;

                // Remove trailing newline
                if line.ends_with('\n') {
                    line.pop();
                    if line.ends_with('\r') {
                        line.pop();
                    }
                }

                Some(Ok(FileLine {
                    content: line,
                    line_number: self.line_number,
                    offset: line_offset,
                }))
            }
            Err(e) => Some(Err(Error::Io {
                path: self.path.clone(),
                source: e,
            })),
        }
    }
}

/// Extension trait for streaming file content.
pub trait StreamExt {
    /// Stream file content in fixed-size chunks.
    ///
    /// This is memory-efficient for very large files.
    fn stream_chunks(&self, chunk_size: usize) -> Result<ContentStream>;

    /// Stream file content line by line.
    fn stream_lines(&self) -> Result<LineStream>;
}

impl<P: AsRef<Path>> StreamExt for P {
    fn stream_chunks(&self, chunk_size: usize) -> Result<ContentStream> {
        ContentStream::new(self, chunk_size)
    }

    fn stream_lines(&self) -> Result<LineStream> {
        LineStream::new(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_content_stream() {
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "Hello, World! This is a test file.").unwrap();

        let mut stream = ContentStream::new(file.path(), 10).unwrap();

        let mut chunks = Vec::new();
        while let Some(chunk) = stream.next_chunk().unwrap() {
            chunks.push(chunk);
        }

        assert_eq!(chunks.len(), 4);
        assert!(!chunks[0].is_last);
        assert!(!chunks[1].is_last);
        assert!(!chunks[2].is_last);
        assert!(chunks[3].is_last);
    }

    #[test]
    fn test_line_stream() {
        let mut file = NamedTempFile::new().unwrap();
        writeln!(file, "Line 1").unwrap();
        writeln!(file, "Line 2").unwrap();
        writeln!(file, "Line 3").unwrap();

        let stream = LineStream::new(file.path()).unwrap();
        let lines: Vec<_> = stream.collect::<Result<Vec<_>>>().unwrap();

        assert_eq!(lines.len(), 3);
        assert_eq!(lines[0].content, "Line 1");
        assert_eq!(lines[0].line_number, 1);
        assert_eq!(lines[1].content, "Line 2");
        assert_eq!(lines[1].line_number, 2);
        assert_eq!(lines[2].content, "Line 3");
        assert_eq!(lines[2].line_number, 3);
    }

    #[test]
    fn test_stream_ext() {
        let mut file = NamedTempFile::new().unwrap();
        write!(file, "Test content").unwrap();

        // Use the extension trait
        let stream = file.path().stream_chunks(5).unwrap();
        let chunks: Vec<_> = stream.collect::<Result<Vec<_>>>().unwrap();
        assert_eq!(chunks.len(), 3);
    }
}
