use crate::commands::{Command, CommandResult};
use crate::services::GitService;
use crate::app::{AppState, Action, reducer};
use crate::errors::CommandError;
use crate::git::parsers::diff::{DiffParseResult, DiffHunk};
use tracing::instrument;
use std::collections::HashSet;

const HEADER_PREFIX: &str = "diff --git a/";

fn file_header(path: &str) -> String {
    format!("{HEADER_PREFIX}{0} b/{0}\n--- a/{0}\n+++ b/{0}\n", path)
}

fn build_hunk_patch(path: &str, hunk: &DiffHunk) -> String {
    let mut buf = String::new();
    buf.push_str(&file_header(path));
    for line in &hunk.lines {
        buf.push_str(&line.content);
        buf.push('\n');
    }
    buf
}

fn build_lines_patch(path: &str, raw: &str, selected: &HashSet<usize>) -> Result<String, String> {
    if selected.is_empty() {
        return Err("No selected lines".into());
    }
    let mut records: Vec<(usize, String, Option<i32>, Option<i32>)> = Vec::new();
    let mut old_lineno = 0;
    let mut new_lineno = 0;
    for (idx, line) in raw.lines().enumerate() {
        if line.starts_with("@@") {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 3 {
                if let Some(old) = parts.get(1) {
                    let nums: Vec<&str> = old.trim_start_matches('-').split(',').collect();
                    old_lineno = nums.get(0).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
                }
                if let Some(newn) = parts.get(2) {
                    let nums: Vec<&str> = newn.trim_start_matches('+').split(',').collect();
                    new_lineno = nums.get(0).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
                }
            }
        } else if line.starts_with('+') {
            let ln = new_lineno;
            new_lineno += 1;
            records.push((idx, line.to_string(), None, Some(ln)));
        } else if line.starts_with('-') {
            let ln = old_lineno;
            old_lineno += 1;
            records.push((idx, line.to_string(), Some(ln), None));
        } else if line.starts_with(' ') {
            old_lineno += 1;
            new_lineno += 1;
        }
    }

    let mut selected_records: Vec<(usize, String, Option<i32>, Option<i32>)> = records
        .into_iter()
        .filter(|(idx, _, _, _)| selected.contains(idx))
        .collect();
    selected_records.sort_by_key(|(idx, _, _, _)| *idx);
    if selected_records.is_empty() {
        return Err("Selected lines are not diff lines".into());
    }

    // group contiguous by idx
    let mut buf = String::new();
    buf.push_str(&file_header(path));

    let mut groups: Vec<Vec<(usize, String, Option<i32>, Option<i32>)>> = Vec::new();
    for rec in selected_records {
        // Defensive: check if groups is empty or if last group's last record exists
        let should_start_new_group = groups.is_empty() || {
            groups.last()
                .and_then(|g| g.last())
                .map(|last_rec| rec.0 != last_rec.0 + 1)
                .unwrap_or(true)
        };
        
        if should_start_new_group {
            groups.push(vec![rec]);
        } else if let Some(last) = groups.last_mut() {
            last.push(rec);
        }
    }

    for g in groups {
        let old_start = g.iter().filter_map(|(_, _, o, _)| *o).min().unwrap_or(0);
        let new_start = g.iter().filter_map(|(_, _, _, n)| *n).min().unwrap_or(0);
        let old_count = g.iter().filter(|(_, _, o, _)| o.is_some()).count();
        let new_count = g.iter().filter(|(_, _, _, n)| n.is_some()).count();
        buf.push_str(&format!("@@ -{},{} +{},{} @@\n", old_start, old_count.max(1), new_start, new_count.max(1)));
        for (_, line, _, _) in g {
            buf.push_str(&line);
            buf.push('\n');
        }
    }

    Ok(buf)
}

pub struct StageHunkCommand {
    pub hunk_index: usize,
    pub parsed: DiffParseResult,
    pub path: String,
    pub raw_diff: String,
}

impl Command for StageHunkCommand {
    #[instrument(skip(self, git, state))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("stage hunk…".into())));
        let patch = if let Some(p) = self
            .parsed
            .files
            .first()
            .and_then(|f| f.hunks.get(self.hunk_index))
            .map(|h| build_hunk_patch(&self.path, h))
        {
            p
        } else {
            // fallback: apply full diff
            format!("{}{}", file_header(&self.path), self.raw_diff)
        };
        match git.apply_patch(&state.repo_path, &patch, true, false) {
            Ok(_) => {
                new_state = reducer(new_state, Action::AppendOpLog("hunk staged".into()));
                new_state = reducer(new_state, Action::SetRefreshing(true));
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("stage hunk error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("stage hunk error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

pub struct UnstageHunkCommand {
    pub hunk_index: usize,
    pub parsed: DiffParseResult,
    pub path: String,
    pub raw_diff: String,
}

impl Command for UnstageHunkCommand {
    #[instrument(skip(self, git, state))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("unstage hunk…".into())));
        let patch = if let Some(p) = self
            .parsed
            .files
            .first()
            .and_then(|f| f.hunks.get(self.hunk_index))
            .map(|h| build_hunk_patch(&self.path, h))
        {
            p
        } else {
            format!("{}{}", file_header(&self.path), self.raw_diff)
        };
        match git.apply_patch(&state.repo_path, &patch, true, true) {
            Ok(_) => {
                new_state = reducer(new_state, Action::AppendOpLog("hunk unstaged".into()));
                new_state = reducer(new_state, Action::SetRefreshing(true));
            }
            Err(e) => {
                new_state = reducer(new_state, Action::AppendOpLog(format!("unstage hunk error: {e}")));
                new_state = reducer(new_state, Action::SetStatusError(Some(format!("unstage hunk error: {e}"))));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

pub struct StageLinesCommand {
    pub raw_diff: String,
    pub path: String,
    pub selected_lines: HashSet<usize>,
}

impl Command for StageLinesCommand {
    #[instrument(skip(self, git, state))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("stage lines…".into())));
        match build_lines_patch(&self.path, &self.raw_diff, &self.selected_lines) {
            Ok(patch) => match git.apply_patch(&state.repo_path, &patch, true, false) {
                Ok(_) => {
                    new_state = reducer(new_state, Action::AppendOpLog("lines staged".into()));
                    new_state = reducer(new_state, Action::SetRefreshing(true));
                }
                Err(e) => {
                    new_state = reducer(new_state, Action::AppendOpLog(format!("stage lines error: {e}")));
                    new_state = reducer(new_state, Action::SetStatusError(Some(format!("stage lines error: {e}"))));
                }
            },
            Err(msg) => {
                new_state = reducer(new_state, Action::SetStatusError(Some(msg)));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

pub struct UnstageLinesCommand {
    pub raw_diff: String,
    pub path: String,
    pub selected_lines: HashSet<usize>,
}

impl Command for UnstageLinesCommand {
    #[instrument(skip(self, git, state))]
    fn execute(
        &self,
        git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        new_state = reducer(new_state, Action::SetOpStatus(Some("unstage lines…".into())));
        match build_lines_patch(&self.path, &self.raw_diff, &self.selected_lines) {
            Ok(patch) => match git.apply_patch(&state.repo_path, &patch, true, true) {
                Ok(_) => {
                    new_state = reducer(new_state, Action::AppendOpLog("lines unstaged".into()));
                    new_state = reducer(new_state, Action::SetRefreshing(true));
                }
                Err(e) => {
                    new_state = reducer(new_state, Action::AppendOpLog(format!("unstage lines error: {e}")));
                    new_state = reducer(new_state, Action::SetStatusError(Some(format!("unstage lines error: {e}"))));
                }
            },
            Err(msg) => {
                new_state = reducer(new_state, Action::SetStatusError(Some(msg)));
            }
        }
        new_state = reducer(new_state, Action::SetOpStatus(None));
        Ok(CommandResult::StateUpdate(new_state))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::git::parsers::diff::{DiffLine, DiffLineKind};

    #[test]
    fn build_lines_patch_single_group() {
        let raw = "\
@@ -1,3 +1,3 @@
 line1
-line2
+line2-changed
 line3
";
        let mut selected = HashSet::new();
        // Indexing matches raw.lines() enumerate; pick the added line.
        selected.insert(3);
        let patch = build_lines_patch("file.txt", raw, &selected).expect("patch");
        assert!(patch.contains("diff --git a/file.txt b/file.txt"));
        assert!(patch.contains("@@"));
        assert!(patch.contains("+line2-changed"));
    }

    #[test]
    fn build_lines_patch_multiple_groups() {
        let raw = "\
@@ -1,6 +1,6 @@
 line1
-line2
+line2-changed
 line3
-line4
+line4-changed
 line5
";
        let mut selected = HashSet::new();
        // lines indexes in enumerate
        selected.insert(3); // line2-changed
        selected.insert(6); // line4-changed
        let patch = build_lines_patch("file.txt", raw, &selected).expect("patch");
        // Two separate hunks should be present.
        assert!(patch.matches("@@").count() >= 2);
        assert!(patch.contains("+line2-changed"));
        assert!(patch.contains("+line4-changed"));
    }

    #[test]
    fn build_lines_patch_rejects_non_diff_line() {
        let raw = "\
@@ -1,3 +1,3 @@
 line1
-line2
+line2-changed
 line3
";
        let mut selected = HashSet::new();
        // Select a context line index (not recorded as diff line).
        selected.insert(1);
        let err = build_lines_patch("file.txt", raw, &selected).unwrap_err();
        assert!(err.contains("Selected lines are not diff lines"));
    }

    #[test]
    fn build_hunk_patch_contains_hunk_lines() {
        let hunk = DiffHunk {
            header: "@@ -1,1 +1,1 @@".to_string(),
            old_start: 1,
            old_lines: 1,
            new_start: 1,
            new_lines: 1,
            lines: vec![
                DiffLine {
                    kind: DiffLineKind::HunkHeader,
                    content: "@@ -1,1 +1,1 @@".to_string(),
                    old_lineno: None,
                    new_lineno: None,
                },
                DiffLine {
                    kind: DiffLineKind::Delete,
                    content: "-old".to_string(),
                    old_lineno: Some(1),
                    new_lineno: None,
                },
                DiffLine {
                    kind: DiffLineKind::Add,
                    content: "+new".to_string(),
                    old_lineno: None,
                    new_lineno: Some(1),
                },
            ],
        };
        let patch = build_hunk_patch("file.txt", &hunk);
        assert!(patch.contains("diff --git a/file.txt b/file.txt"));
        assert!(patch.contains("@@ -1,1 +1,1 @@"));
        assert!(patch.contains("-old"));
        assert!(patch.contains("+new"));
    }
}