use std::{io::ErrorKind, path::Path};

use anyhow::Result;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom};

use crate::hook::Hook;
use crate::hooks::HookOutput;
use crate::hooks::pre_commit_hooks::{FilenamesArgs, parse_hook_args, run_file_checks};
use crate::run::INTERNAL_CONCURRENCY;

const UTF8_BOM: &[u8] = b"\xef\xbb\xbf";
const BUFFER_SIZE: usize = 8192; // 8KB buffer for streaming

/// Runs the `fix-byte-order-marker` hook.
pub(crate) async fn run(hook: &Hook, filenames: &[&Path]) -> Result<HookOutput> {
    let args: FilenamesArgs = parse_hook_args(hook)?;
    run_file_checks(
        &args.filenames,
        filenames,
        *INTERNAL_CONCURRENCY,
        |filename| fix_file(hook.project().relative_path(), filename),
    )
    .await
}

async fn fix_file(file_base: &Path, filename: &Path) -> Result<HookOutput> {
    let file_path = file_base.join(filename);

    let mut file = fs_err::tokio::OpenOptions::new()
        .read(true)
        .write(true)
        .open(&file_path)
        .await?;

    let mut bom_buffer = [0u8; 3];
    match file.read_exact(&mut bom_buffer).await {
        Ok(_) => {}
        Err(err) if err.kind() == ErrorKind::UnexpectedEof => {
            return Ok(HookOutput::unchanged(0, Vec::new()));
        }
        Err(err) => return Err(err.into()),
    }
    if bom_buffer != UTF8_BOM {
        return Ok(HookOutput::unchanged(0, Vec::new()));
    }

    let file_len = file.seek(SeekFrom::End(0)).await?;
    if file_len == UTF8_BOM.len() as u64 {
        file.set_len(0).await?;
    } else {
        // Shift the payload in place so large files do not need a full second buffer.
        shift_file_left(&mut file, file_len, UTF8_BOM.len() as u64).await?;
    }

    Ok(HookOutput::known(
        1,
        format!("{}: removed byte-order marker\n", filename.display()).into_bytes(),
        true,
    ))
}

async fn shift_file_left(file: &mut fs_err::tokio::File, file_len: u64, offset: u64) -> Result<()> {
    let mut buf = vec![0u8; BUFFER_SIZE];
    let mut read_pos = offset;
    let mut write_pos = 0;

    while read_pos < file_len {
        // Read after the BOM and rewrite earlier in the same file.
        let remaining = usize::try_from(file_len - read_pos)?;
        let chunk_len = BUFFER_SIZE.min(remaining);
        file.seek(SeekFrom::Start(read_pos)).await?;
        file.read_exact(&mut buf[..chunk_len]).await?;
        file.seek(SeekFrom::Start(write_pos)).await?;
        file.write_all(&buf[..chunk_len]).await?;
        read_pos += chunk_len as u64;
        write_pos += chunk_len as u64;
    }

    file.set_len(file_len - offset).await?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;
    use tempfile::tempdir;

    async fn create_test_file(
        dir: &tempfile::TempDir,
        name: &str,
        content: &[u8],
    ) -> Result<PathBuf> {
        let file_path = dir.path().join(name);
        fs_err::tokio::write(&file_path, content).await?;
        Ok(file_path)
    }

    #[tokio::test]
    async fn test_file_with_bom() -> Result<()> {
        let dir = tempdir()?;
        let content = b"\xef\xbb\xbfHello, World!";
        let file_path = create_test_file(&dir, "with_bom.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 1);
        let output_str = String::from_utf8_lossy(&result.output);
        assert!(output_str.contains("removed byte-order marker"));

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, b"Hello, World!");

        Ok(())
    }

    #[tokio::test]
    async fn test_file_without_bom() -> Result<()> {
        let dir = tempdir()?;
        let content = b"Hello, World!";
        let file_path = create_test_file(&dir, "without_bom.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 0);
        assert!(result.output.is_empty());

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, content);

        Ok(())
    }

    #[tokio::test]
    async fn test_empty_file() -> Result<()> {
        let dir = tempdir()?;
        let content = b"";
        let file_path = create_test_file(&dir, "empty.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 0);
        assert!(result.output.is_empty());

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, content);

        Ok(())
    }

    #[tokio::test]
    async fn test_file_shorter_than_bom() -> Result<()> {
        let dir = tempdir()?;
        let content = b"Hi";
        let file_path = create_test_file(&dir, "short.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 0);
        assert!(result.output.is_empty());

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, content);

        Ok(())
    }

    #[tokio::test]
    async fn test_file_with_partial_bom() -> Result<()> {
        let dir = tempdir()?;
        let content = b"\xef\xbbHello"; // Only first 2 bytes of BOM
        let file_path = create_test_file(&dir, "partial_bom.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 0);
        assert!(result.output.is_empty());

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, content);

        Ok(())
    }

    #[tokio::test]
    async fn test_bom_only_file() -> Result<()> {
        let dir = tempdir()?;
        let content = b"\xef\xbb\xbf";
        let file_path = create_test_file(&dir, "bom_only.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 1);
        let output_str = String::from_utf8_lossy(&result.output);
        assert!(output_str.contains("removed byte-order marker"));

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, b"");

        Ok(())
    }

    #[tokio::test]
    async fn test_utf8_content_with_bom() -> Result<()> {
        let dir = tempdir()?;
        let content = b"\xef\xbb\xbf\xe4\xb8\xad\xe6\x96\x87"; // BOM + Chinese characters "中文"
        let file_path = create_test_file(&dir, "utf8_with_bom.txt", content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 1);
        let output_str = String::from_utf8_lossy(&result.output);
        assert!(output_str.contains("removed byte-order marker"));

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content, b"\xe4\xb8\xad\xe6\x96\x87"); // Just the Chinese characters

        // Verify we can still read it as valid UTF-8
        let text = String::from_utf8(new_content)?;
        assert_eq!(text, "中文");

        Ok(())
    }

    #[tokio::test]
    async fn test_large_file_streaming() -> Result<()> {
        let dir = tempdir()?;

        // Create a large file (>64KB) with BOM
        let mut content = Vec::with_capacity(100_000);
        content.extend_from_slice(b"\xef\xbb\xbf");
        content.extend(b"x".repeat(100_000));

        let file_path = create_test_file(&dir, "large_with_bom.txt", &content).await?;

        let result = fix_file(Path::new(""), &file_path).await?;

        assert_eq!(result.exit_status, 1);
        let output_str = String::from_utf8_lossy(&result.output);
        assert!(output_str.contains("removed byte-order marker"));

        let new_content = fs_err::tokio::read(&file_path).await?;
        assert_eq!(new_content.len(), 100_000);
        assert!(new_content.iter().all(|&b| b == b'x'));

        Ok(())
    }
}
