use std::path::Path;

use anyhow::anyhow;

mod applicator;
mod error;
mod matcher;
mod parser;
mod path;
mod semantic;

pub use error::PatchError;
#[doc(hidden)]
pub(crate) use semantic::resolve_ast_grep_binary_path;
#[doc(hidden)]
pub use semantic::{AstGrepBinaryOverrideGuard, set_ast_grep_binary_override_for_tests};

/// Represents a single diff line inside a patch hunk.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchLine {
    Context(String),
    Addition(String),
    Removal(String),
}

impl PatchLine {
    pub fn as_str(&self) -> &str {
        match self {
            PatchLine::Context(text) | PatchLine::Addition(text) | PatchLine::Removal(text) => text,
        }
    }
}

/// Represents a chunk of changes within an update operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchChunk {
    pub change_context: Option<String>,
    pub lines: Vec<PatchLine>,
    pub is_end_of_file: bool,
}

impl PatchChunk {
    pub fn lines(&self) -> &[PatchLine] {
        &self.lines
    }

    pub fn change_context(&self) -> Option<&str> {
        self.change_context.as_deref()
    }

    pub fn is_end_of_file(&self) -> bool {
        self.is_end_of_file
    }

    pub(crate) fn to_segments(&self) -> (Vec<String>, Vec<String>) {
        let cap = self.lines.len();
        let mut old_lines = Vec::with_capacity(cap);
        let mut new_lines = Vec::with_capacity(cap);

        for line in &self.lines {
            match line {
                PatchLine::Context(text) => {
                    old_lines.push(text.clone());
                    new_lines.push(text.clone());
                }
                PatchLine::Addition(text) => {
                    new_lines.push(text.clone());
                }
                PatchLine::Removal(text) => {
                    old_lines.push(text.clone());
                }
            }
        }

        (old_lines, new_lines)
    }

    pub(crate) fn has_old_lines(&self) -> bool {
        self.lines
            .iter()
            .any(|line| matches!(line, PatchLine::Context(_) | PatchLine::Removal(_)))
    }

    pub fn parse_line_number(&self) -> Option<usize> {
        let ctx = self.change_context()?;
        // Format is typically: -old_start,old_count +new_start,new_count @@
        let parts: Vec<&str> = ctx.split_whitespace().collect();
        let old_part = if !parts.is_empty() && parts[0].starts_with('-') {
            Some(parts[0])
        } else if parts.len() >= 2 && parts[1].starts_with('-') {
            Some(parts[1])
        } else {
            None
        }?;

        let range_str = old_part.strip_prefix('-')?;
        let range_parts: Vec<&str> = range_str.split(',').collect();
        let start_str = range_parts.first()?;
        start_str.parse::<usize>().ok()
    }
}

pub type PatchHunk = PatchChunk;

/// Represents a patch operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatchOperation {
    AddFile {
        path: String,
        content: String,
    },
    DeleteFile {
        path: String,
    },
    UpdateFile {
        path: String,
        new_path: Option<String>,
        chunks: Vec<PatchChunk>,
    },
}

/// Represents a complete patch comprised of multiple operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Patch {
    operations: Vec<PatchOperation>,
}

impl Patch {
    pub fn parse(input: &str) -> anyhow::Result<Self> {
        let operations = parser::parse(input).map_err(|err| anyhow!(err))?;
        Ok(Self { operations })
    }

    pub fn operations(&self) -> &[PatchOperation] {
        &self.operations
    }

    pub fn is_empty(&self) -> bool {
        self.operations.is_empty()
    }

    pub fn into_operations(self) -> Vec<PatchOperation> {
        self.operations
    }

    pub async fn apply(&self, root: &Path) -> anyhow::Result<Vec<String>> {
        applicator::apply(root, &self.operations)
            .await
            .map_err(|err| anyhow!(err))
    }
}

pub async fn render_patch_update_content(
    source_path: &Path,
    content: &str,
    chunks: &[PatchChunk],
    path: &str,
) -> anyhow::Result<String> {
    applicator::render_updated_content(source_path, content, chunks, path)
        .await
        .map_err(|err| anyhow!(err))
}

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

    #[test]
    fn parse_add_file() {
        let patch = Patch::parse("*** Begin Patch\n*** Add File: hello.txt\n+hello\n*** End Patch")
            .unwrap();
        assert_eq!(patch.operations().len(), 1);
        matches!(patch.operations()[0], PatchOperation::AddFile { .. });
    }

    #[tokio::test]
    async fn apply_add_file() {
        let temp_dir = TempDir::new().unwrap();
        let patch =
            Patch::parse("*** Begin Patch\n*** Add File: file.txt\n+content\n*** End Patch")
                .unwrap();

        let result = patch.apply(temp_dir.path()).await.unwrap();
        assert_eq!(
            result,
            vec!["[1/1] Added file: file.txt (8 bytes)".to_string()]
        );
        let written = tokio::fs::read_to_string(temp_dir.path().join("file.txt"))
            .await
            .unwrap();
        assert_eq!(written, "content\n");
    }
}
