use crate::model::{RawRevisionIds, TrackedChange, TrackedChangeKind};
use crate::{ExtractOptions, TrackChangesMode};

pub(crate) fn resolve_tracked_changes(
    raw_changes: Vec<TrackedChange>,
    opts: &ExtractOptions,
) -> (Vec<TrackedChange>, Vec<TrackedChange>) {
    let raw_changes = annotate_move_pairs(raw_changes);

    match opts.track_changes_mode {
        TrackChangesMode::Raw => (finalize_changes(raw_changes, opts.include_raw_ids), Vec::new()),
        TrackChangesMode::Paired => (
            finalize_changes(pair_adjacent_changes(&raw_changes), opts.include_raw_ids),
            Vec::new(),
        ),
        TrackChangesMode::Both => (
            finalize_changes(pair_adjacent_changes(&raw_changes), opts.include_raw_ids),
            finalize_changes(raw_changes, opts.include_raw_ids),
        ),
    }
}

fn pair_adjacent_changes(raw_changes: &[TrackedChange]) -> Vec<TrackedChange> {
    let mut paired = Vec::new();
    let mut index = 0usize;

    while index < raw_changes.len() {
        if let Some(next) = raw_changes.get(index + 1)
            && let Some(replacement) = maybe_pair_replacement(&raw_changes[index], next)
        {
            paired.push(replacement);
            index += 2;
            continue;
        }

        paired.push(raw_changes[index].clone());
        index += 1;
    }

    paired
}

fn maybe_pair_replacement(first: &TrackedChange, second: &TrackedChange) -> Option<TrackedChange> {
    let (old_text, new_text, delete_id, insert_id) = match (&first.kind, &second.kind) {
        (TrackedChangeKind::Delete { text: old_text }, TrackedChangeKind::Insert { text: new_text }) => {
            (
                old_text.clone(),
                new_text.clone(),
                first.raw_revision_ids.delete.clone(),
                second.raw_revision_ids.insert.clone(),
            )
        }
        (TrackedChangeKind::Insert { text: new_text }, TrackedChangeKind::Delete { text: old_text }) => {
            (
                old_text.clone(),
                new_text.clone(),
                second.raw_revision_ids.delete.clone(),
                first.raw_revision_ids.insert.clone(),
            )
        }
        _ => return None,
    };

    if first.author != second.author || first.date != second.date || first.block_ids != second.block_ids {
        return None;
    }

    let mut replacement = first.clone();
    replacement.kind = TrackedChangeKind::Replacement { old_text, new_text };
    replacement.raw_revision_ids = RawRevisionIds {
        insert: insert_id,
        delete: delete_id,
        ..RawRevisionIds::default()
    };
    replacement.id = replacement
        .raw_revision_ids
        .insert
        .as_deref()
        .map(|id| format!("body:tc:rep:{id}"))
        .or_else(|| {
            replacement
                .raw_revision_ids
                .delete
                .as_deref()
                .map(|id| format!("body:tc:rep:{id}"))
        })
        .unwrap_or_else(|| format!("body:tc:rep:{}", paired_suffix(&first.id, &second.id)));
    replacement.excerpt = excerpt_for_kind(&replacement.kind);
    replacement.move_pair_id = None;

    Some(replacement)
}

fn annotate_move_pairs(raw_changes: Vec<TrackedChange>) -> Vec<TrackedChange> {
    use std::collections::HashMap;

    let mut move_pairs = HashMap::<String, String>::new();
    let mut next_pair = 1usize;

    for change in &raw_changes {
        let key = match &change.kind {
            TrackedChangeKind::MoveFrom { .. } => change.raw_revision_ids.move_from.as_deref(),
            TrackedChangeKind::MoveTo { .. } => change.raw_revision_ids.move_to.as_deref(),
            _ => None,
        };

        if let Some(key) = key {
            move_pairs.entry(key.to_string()).or_insert_with(|| {
                let id = format!("mv-{next_pair}");
                next_pair += 1;
                id
            });
        }
    }

    raw_changes
        .into_iter()
        .map(|mut change| {
            let key = match &change.kind {
                TrackedChangeKind::MoveFrom { .. } => change.raw_revision_ids.move_from.as_deref(),
                TrackedChangeKind::MoveTo { .. } => change.raw_revision_ids.move_to.as_deref(),
                _ => None,
            };

            change.move_pair_id = key.and_then(|key| move_pairs.get(key).cloned());
            change.excerpt = excerpt_for_kind(&change.kind);

            change
        })
        .collect()
}

fn finalize_changes(changes: Vec<TrackedChange>, include_raw_ids: bool) -> Vec<TrackedChange> {
    changes
        .into_iter()
        .map(|mut change| {
            change.excerpt = excerpt_for_kind(&change.kind);
            if !include_raw_ids {
                change.raw_revision_ids = RawRevisionIds::default();
            }
            change
        })
        .collect()
}

fn excerpt_for_kind(kind: &TrackedChangeKind) -> Option<String> {
    match kind {
        TrackedChangeKind::Insert { text }
        | TrackedChangeKind::Delete { text }
        | TrackedChangeKind::MoveFrom { text }
        | TrackedChangeKind::MoveTo { text } => excerpt_from_text(text),
        TrackedChangeKind::Replacement { old_text, new_text } => {
            let summary = format!("{old_text} -> {new_text}");
            excerpt_from_text(&summary)
        }
        TrackedChangeKind::Format {
            previous_props_summary,
        } => excerpt_from_text(previous_props_summary),
    }
}

fn excerpt_from_text(text: &str) -> Option<String> {
    let trimmed = text.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.chars().take(80).collect())
    }
}

fn paired_suffix(first_id: &str, second_id: &str) -> String {
    first_id
        .rsplit(':')
        .next()
        .zip(second_id.rsplit(':').next())
        .map(|(first, second)| format!("{first}-{second}"))
        .unwrap_or_else(|| "paired".to_string())
}

#[cfg(test)]
mod tests {
    use chrono::{TimeZone, Utc};

    use super::resolve_tracked_changes;
    use crate::model::{RawRevisionIds, TrackedChange, TrackedChangeKind};
    use crate::{ExtractOptions, TrackChangesMode};

    #[test]
    fn pairs_adjacent_delete_and_insert() {
        let raw = vec![
            delete_change("body:tc:del:3", "old", Some("3"), Some("A")),
            insert_change("body:tc:ins:4", "new", Some("4"), Some("A")),
        ];

        let (changes, raw_changes) = resolve_tracked_changes(
            raw,
            &ExtractOptions {
                track_changes_mode: TrackChangesMode::Paired,
                include_raw_ids: true,
                ..ExtractOptions::default()
            },
        );

        assert!(raw_changes.is_empty());
        assert_eq!(changes.len(), 1);
        assert!(matches!(
            changes[0].kind,
            TrackedChangeKind::Replacement { .. }
        ));
        assert_eq!(changes[0].raw_revision_ids.delete.as_deref(), Some("3"));
        assert_eq!(changes[0].raw_revision_ids.insert.as_deref(), Some("4"));
    }

    #[test]
    fn replacement_id_stays_stable_without_raw_ids() {
        let raw = vec![
            delete_change("body:tc:del:3", "old", Some("3"), Some("A")),
            insert_change("body:tc:ins:4", "new", Some("4"), Some("A")),
        ];

        let (changes, _) = resolve_tracked_changes(
            raw,
            &ExtractOptions {
                track_changes_mode: TrackChangesMode::Paired,
                include_raw_ids: false,
                ..ExtractOptions::default()
            },
        );

        assert_eq!(changes.len(), 1);
        assert!(matches!(
            changes[0].kind,
            TrackedChangeKind::Replacement { .. }
        ));
        assert_eq!(changes[0].id, "body:tc:rep:4");
        assert_eq!(changes[0].raw_revision_ids, RawRevisionIds::default());
    }

    #[test]
    fn preserves_unpaired_change_in_paired_mode() {
        let raw = vec![insert_change("body:tc:ins:4", "new", Some("4"), Some("A"))];

        let (changes, _) = resolve_tracked_changes(raw, &ExtractOptions::default());

        assert_eq!(changes.len(), 1);
        assert!(matches!(changes[0].kind, TrackedChangeKind::Insert { .. }));
    }

    #[test]
    fn links_move_pairs_across_blocks() {
        let raw = vec![
            move_from_change("body:tc:mvf:7", "moved", Some("7"), "body:block:000001"),
            move_to_change("body:tc:mvt:7", "moved", Some("7"), "body:block:000002"),
        ];

        let (changes, _) = resolve_tracked_changes(
            raw,
            &ExtractOptions {
                track_changes_mode: TrackChangesMode::Raw,
                include_raw_ids: true,
                ..ExtractOptions::default()
            },
        );

        assert_eq!(changes.len(), 2);
        assert_eq!(changes[0].move_pair_id.as_deref(), Some("mv-1"));
        assert_eq!(changes[1].move_pair_id.as_deref(), Some("mv-1"));
    }

    #[test]
    fn both_mode_keeps_raw_changes_separately() {
        let raw = vec![
            delete_change("body:tc:del:3", "old", Some("3"), Some("A")),
            insert_change("body:tc:ins:4", "new", Some("4"), Some("A")),
        ];

        let (changes, raw_changes) = resolve_tracked_changes(
            raw,
            &ExtractOptions {
                track_changes_mode: TrackChangesMode::Both,
                include_raw_ids: false,
                ..ExtractOptions::default()
            },
        );

        assert_eq!(changes.len(), 1);
        assert_eq!(raw_changes.len(), 2);
        assert_eq!(changes[0].raw_revision_ids, RawRevisionIds::default());
        assert_eq!(raw_changes[0].raw_revision_ids, RawRevisionIds::default());
    }

    fn insert_change(id: &str, text: &str, raw_id: Option<&str>, author: Option<&str>) -> TrackedChange {
        TrackedChange {
            id: id.to_string(),
            kind: TrackedChangeKind::Insert {
                text: text.to_string(),
            },
            author: author.map(str::to_string),
            date: Some(Utc.with_ymd_and_hms(2026, 5, 29, 10, 0, 0).unwrap()),
            block_ids: vec!["body:block:000001".to_string()],
            excerpt: None,
            move_pair_id: None,
            raw_revision_ids: RawRevisionIds {
                insert: raw_id.map(str::to_string),
                ..RawRevisionIds::default()
            },
        }
    }

    fn delete_change(id: &str, text: &str, raw_id: Option<&str>, author: Option<&str>) -> TrackedChange {
        TrackedChange {
            id: id.to_string(),
            kind: TrackedChangeKind::Delete {
                text: text.to_string(),
            },
            author: author.map(str::to_string),
            date: Some(Utc.with_ymd_and_hms(2026, 5, 29, 10, 0, 0).unwrap()),
            block_ids: vec!["body:block:000001".to_string()],
            excerpt: None,
            move_pair_id: None,
            raw_revision_ids: RawRevisionIds {
                delete: raw_id.map(str::to_string),
                ..RawRevisionIds::default()
            },
        }
    }

    fn move_from_change(id: &str, text: &str, raw_id: Option<&str>, block_id: &str) -> TrackedChange {
        TrackedChange {
            id: id.to_string(),
            kind: TrackedChangeKind::MoveFrom {
                text: text.to_string(),
            },
            author: Some("A".to_string()),
            date: Some(Utc.with_ymd_and_hms(2026, 5, 29, 10, 0, 0).unwrap()),
            block_ids: vec![block_id.to_string()],
            excerpt: None,
            move_pair_id: None,
            raw_revision_ids: RawRevisionIds {
                move_from: raw_id.map(str::to_string),
                ..RawRevisionIds::default()
            },
        }
    }

    fn move_to_change(id: &str, text: &str, raw_id: Option<&str>, block_id: &str) -> TrackedChange {
        TrackedChange {
            id: id.to_string(),
            kind: TrackedChangeKind::MoveTo {
                text: text.to_string(),
            },
            author: Some("A".to_string()),
            date: Some(Utc.with_ymd_and_hms(2026, 5, 29, 10, 0, 0).unwrap()),
            block_ids: vec![block_id.to_string()],
            excerpt: None,
            move_pair_id: None,
            raw_revision_ids: RawRevisionIds {
                move_to: raw_id.map(str::to_string),
                ..RawRevisionIds::default()
            },
        }
    }
}
