//! Core reconstruction algorithm: greedy maximum-overlap assembly with position constraints.
//!
//! Uses direct scan like the original Python but implemented efficiently in Rust.
//! The Rust advantage comes from zero-copy slice comparisons, no GC overhead,
//! and rayon parallelism at the article level — not from algorithmic changes
//! that could reduce reconstruction quality.

use crate::fragment::Fragment;

/// Result of reconstructing an article from fragments.
#[derive(Debug)]
pub struct AssemblyResult {
    /// The reconstructed word sequence.
    pub words: Vec<String>,
    /// Number of fragments successfully merged.
    pub fragments_used: usize,
    /// Total fragments available.
    pub fragments_total: usize,
}

/// Find the best overlap between the end of `result` and the start of `candidate`.
/// Returns the overlap length (0 if none found).
#[inline]
fn suffix_prefix_overlap(result: &[String], candidate: &[String]) -> usize {
    let max_k = result.len().min(candidate.len());
    for k in (1..=max_k).rev() {
        if result[result.len() - k..] == candidate[..k] {
            return k;
        }
    }
    0
}

/// Find the best overlap between the start of `result` and the end of `candidate`.
/// Returns the overlap length (0 if none found).
#[inline]
fn prefix_suffix_overlap(result: &[String], candidate: &[String]) -> usize {
    let max_k = result.len().min(candidate.len());
    for k in (1..=max_k).rev() {
        if result[..k] == candidate[candidate.len() - k..] {
            return k;
        }
    }
    0
}

/// Reconstruct an article from its fragments using greedy maximum-overlap assembly.
///
/// This mirrors the original Python algorithm exactly but runs much faster
/// because Rust slice comparisons are zero-copy and there's no interpreter overhead.
///
/// Algorithm:
/// 1. Start from the first fragment (lowest position, already sorted by caller).
/// 2. Each iteration, scan all unused fragments:
///    - Check position constraints (append only if pos >= max_pos, prepend only if pos <= min_pos)
///    - Find the fragment with the largest overlap (suffix-prefix for append, prefix-suffix for prepend)
/// 3. Merge the best fragment found.
/// 4. Stop when no fragment can be merged.
pub fn assemble(fragments: &[Fragment]) -> AssemblyResult {
    let n = fragments.len();

    if n == 0 {
        return AssemblyResult {
            words: Vec::new(),
            fragments_used: 0,
            fragments_total: 0,
        };
    }

    if n == 1 {
        return AssemblyResult {
            words: fragments[0].words.clone(),
            fragments_used: 1,
            fragments_total: 1,
        };
    }

    let mut used = vec![false; n];
    used[0] = true;
    let mut result_words: Vec<String> = fragments[0].words.clone();
    let mut min_pos = fragments[0].pos;
    let mut max_pos = fragments[0].pos;
    let mut fragments_used = 1;

    loop {
        let mut best_idx: Option<usize> = None;
        let mut best_overlap: usize = 0;
        let mut best_is_prepend = false;

        for (idx, frag) in fragments.iter().enumerate() {
            if used[idx] || frag.words.is_empty() {
                continue;
            }

            let p = frag.pos;
            let can_append = p >= max_pos;
            let can_prepend = p <= min_pos;

            if !can_append && !can_prepend {
                continue;
            }

            // Try append: result_suffix matches fragment_prefix
            if can_append {
                let overlap = suffix_prefix_overlap(&result_words, &frag.words);
                if overlap > best_overlap {
                    best_overlap = overlap;
                    best_idx = Some(idx);
                    best_is_prepend = false;
                }
            }

            // Try prepend: result_prefix matches fragment_suffix
            if can_prepend {
                let overlap = prefix_suffix_overlap(&result_words, &frag.words);
                if overlap > best_overlap {
                    best_overlap = overlap;
                    best_idx = Some(idx);
                    best_is_prepend = true;
                }
            }
        }

        match best_idx {
            Some(idx) if best_overlap > 0 => {
                used[idx] = true;
                fragments_used += 1;
                let frag = &fragments[idx];

                if best_is_prepend {
                    let flen = frag.words.len();
                    let mut new_words = frag.words[..flen - best_overlap].to_vec();
                    new_words.append(&mut result_words);
                    result_words = new_words;
                    min_pos = min_pos.min(frag.pos);
                } else {
                    result_words.extend_from_slice(&frag.words[best_overlap..]);
                    max_pos = max_pos.max(frag.pos);
                }
            }
            _ => break,
        }
    }

    // Remove circular prefix/suffix overlap
    result_words = remove_circular_overlap(result_words);

    AssemblyResult {
        words: result_words,
        fragments_used,
        fragments_total: n,
    }
}

/// Remove prefix/suffix duplication (same as Python's remove_overlap).
fn remove_circular_overlap(words: Vec<String>) -> Vec<String> {
    let n = words.len();
    if n < 2 {
        return words;
    }

    for k in (1..=n / 2).rev() {
        if words[..k] == words[n - k..] {
            return words[k..].to_vec();
        }
    }

    words
}

/// Clean reconstructed text for output.
pub fn clean_text(words: &[String]) -> String {
    let text = words.join(" ");
    let text = text.replace('|', " ").replace('"', " ");
    let mut result = String::with_capacity(text.len());
    let mut last_was_space = false;
    for ch in text.chars() {
        if ch.is_whitespace() {
            if !last_was_space {
                result.push(' ');
                last_was_space = true;
            }
        } else {
            result.push(ch);
            last_was_space = false;
        }
    }
    result.trim().to_string()
}

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

    fn frag(words: &str, pos: i32) -> Fragment {
        Fragment {
            words: words.split_whitespace().map(String::from).collect(),
            pos,
            date: "2020-01-01".into(),
        }
    }

    #[test]
    fn test_simple_assembly() {
        let fragments = vec![
            frag("The quick brown fox", 0),
            frag("brown fox jumps over", 10),
            frag("jumps over the lazy dog", 20),
        ];
        let result = assemble(&fragments);
        assert_eq!(
            result.words.join(" "),
            "The quick brown fox jumps over the lazy dog"
        );
        assert_eq!(result.fragments_used, 3);
    }

    #[test]
    fn test_single_fragment() {
        let fragments = vec![frag("hello world", 0)];
        let result = assemble(&fragments);
        assert_eq!(result.words.join(" "), "hello world");
    }

    #[test]
    fn test_no_overlap_stops() {
        let fragments = vec![
            frag("hello world", 0),
            frag("completely different text", 50),
        ];
        let result = assemble(&fragments);
        assert_eq!(result.fragments_used, 1);
    }

    #[test]
    fn test_position_constraint() {
        let fragments = vec![
            frag("the beginning of article", 0),
            frag("of article continues here", 10),
            frag("end of the article the beginning", 90),
        ];
        let result = assemble(&fragments);
        assert_eq!(
            result.words.join(" "),
            "the beginning of article continues here"
        );
    }

    #[test]
    fn test_circular_overlap_removal() {
        let words: Vec<String> = "A B C D E A B C"
            .split_whitespace()
            .map(String::from)
            .collect();
        let result = remove_circular_overlap(words);
        assert_eq!(result.join(" "), "D E A B C");
    }

    #[test]
    fn test_empty() {
        let result = assemble(&[]);
        assert!(result.words.is_empty());
    }
}
