use super::MergeStrategy;

pub fn merge(dest: &str, src: &str, strategy: MergeStrategy, id: &str) -> String {
    let block = render_block(id, src);

    if let Some((start, end)) = find_block(dest, id) {
        // Re-install: replace in place regardless of strategy — keeps idempotency.
        let mut out = String::with_capacity(dest.len() + block.len());
        out.push_str(&dest[..start]);
        out.push_str(&block);
        out.push_str(&dest[end..]);
        return out;
    }

    match strategy {
        MergeStrategy::Replace => format!("{block}\n"),
        MergeStrategy::Append => append_block(dest, &block),
        MergeStrategy::Prepend => prepend_block(dest, &block),
    }
}

pub fn remove(dest: &str, id: &str) -> String {
    let Some((start, end)) = find_block(dest, id) else {
        return dest.to_string();
    };
    let before = dest[..start].trim_end_matches('\n');
    let after = dest[end..].trim_start_matches('\n');
    let mut out = String::with_capacity(before.len() + after.len() + 2);
    if !before.is_empty() {
        out.push_str(before);
        out.push_str(if after.is_empty() { "\n" } else { "\n\n" });
    }
    if !after.is_empty() {
        out.push_str(after);
        if !out.ends_with('\n') {
            out.push('\n');
        }
    }
    out
}

/// Render the exact block text we'd write for `id`/`content`.
/// Callers use this to record a pristine-snapshot in state for drift detection.
pub fn render_block(id: &str, content: &str) -> String {
    let trimmed = content.trim_matches('\n');
    format!("<!-- BEGIN aimgr: {id} -->\n{trimmed}\n<!-- END aimgr: {id} -->")
}

/// Extract the current block text for `id` from `haystack`, if present.
pub fn extract_block<'a>(haystack: &'a str, id: &str) -> Option<&'a str> {
    let (start, end) = find_block(haystack, id)?;
    Some(&haystack[start..end])
}

/// Extract just the body inside the BEGIN/END markers (no markers, no surrounding newlines).
pub fn extract_body<'a>(haystack: &'a str, id: &str) -> Option<&'a str> {
    let block = extract_block(haystack, id)?;
    let begin = format!("<!-- BEGIN aimgr: {id} -->");
    let end = format!("<!-- END aimgr: {id} -->");
    let body = block
        .strip_prefix(&begin)?
        .strip_suffix(&end)?
        .trim_matches('\n');
    Some(body)
}

/// Where a `aimgr-custom` region sits relative to the managed content inside a block.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CustomPosition {
    /// Custom region comes before (above) the managed content.
    Top,
    /// Custom region comes after (below) the managed content.
    Bottom,
}

/// Wrap `content` in aimgr-custom markers for `id`.
pub fn render_custom(id: &str, content: &str) -> String {
    let trimmed = content.trim_matches('\n');
    format!("<!-- BEGIN aimgr-custom: {id} -->\n{trimmed}\n<!-- END aimgr-custom: {id} -->")
}

/// Split a block body into (managed_portion, optional custom region).
/// If the body contains matched aimgr-custom markers for `id`, the content inside is
/// returned plus its position (Top / Bottom) relative to the managed text.
pub fn split_body(body: &str, id: &str) -> (String, Option<(CustomPosition, String)>) {
    let begin = format!("<!-- BEGIN aimgr-custom: {id} -->");
    let end = format!("<!-- END aimgr-custom: {id} -->");
    let Some(begin_idx) = body.find(&begin) else {
        return (body.trim_matches('\n').to_string(), None);
    };
    let Some(rel_end) = body[begin_idx..].find(&end) else {
        return (body.trim_matches('\n').to_string(), None);
    };
    let end_idx = begin_idx + rel_end + end.len();
    let inner = body[begin_idx + begin.len()..begin_idx + rel_end]
        .trim_matches('\n')
        .to_string();
    let before = body[..begin_idx].trim_matches('\n');
    let after = body[end_idx..].trim_matches('\n');
    let (position, managed) = if before.is_empty() {
        (CustomPosition::Top, after.to_string())
    } else if after.is_empty() {
        (CustomPosition::Bottom, before.to_string())
    } else {
        // Marker in the middle: keep the "before" text as managed and treat as Bottom.
        (CustomPosition::Bottom, before.to_string())
    };
    (managed, Some((position, inner)))
}

/// Assemble a block body from managed content plus an optional custom region.
pub fn compose_body(managed: &str, custom: Option<(CustomPosition, &str)>, id: &str) -> String {
    let managed = managed.trim_matches('\n');
    match custom {
        None => managed.to_string(),
        Some((CustomPosition::Top, content)) => {
            format!("{}\n\n{}", render_custom(id, content), managed)
        }
        Some((CustomPosition::Bottom, content)) => {
            format!("{}\n\n{}", managed, render_custom(id, content))
        }
    }
}

fn find_block(haystack: &str, id: &str) -> Option<(usize, usize)> {
    let begin = format!("<!-- BEGIN aimgr: {id} -->");
    let end = format!("<!-- END aimgr: {id} -->");
    let start = haystack.find(&begin)?;
    let rel_end_start = haystack[start..].find(&end)?;
    let end_start = start + rel_end_start;
    Some((start, end_start + end.len()))
}

fn append_block(dest: &str, block: &str) -> String {
    if dest.is_empty() {
        return format!("{block}\n");
    }
    let base = dest.trim_end_matches('\n');
    format!("{base}\n\n{block}\n")
}

fn prepend_block(dest: &str, block: &str) -> String {
    if dest.is_empty() {
        return format!("{block}\n");
    }
    let base = dest.trim_start_matches('\n');
    format!("{block}\n\n{base}")
}

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

    const ID: &str = "plannotator";

    fn block(id: &str, content: &str) -> String {
        format!("<!-- BEGIN aimgr: {id} -->\n{content}\n<!-- END aimgr: {id} -->")
    }

    #[test]
    fn append_into_empty_file() {
        let out = merge("", "hello", Append, ID);
        assert_eq!(out, format!("{}\n", block(ID, "hello")));
    }

    #[test]
    fn prepend_into_empty_file() {
        let out = merge("", "hello", Prepend, ID);
        assert_eq!(out, format!("{}\n", block(ID, "hello")));
    }

    #[test]
    fn replace_into_empty_file() {
        let out = merge("", "hello", Replace, ID);
        assert_eq!(out, format!("{}\n", block(ID, "hello")));
    }

    #[test]
    fn append_separates_with_blank_line() {
        let out = merge("# Title\n", "body", Append, ID);
        assert_eq!(out, format!("# Title\n\n{}\n", block(ID, "body")));
    }

    #[test]
    fn prepend_separates_with_blank_line() {
        let out = merge("# Title\n", "body", Prepend, ID);
        assert_eq!(out, format!("{}\n\n# Title\n", block(ID, "body")));
    }

    #[test]
    fn replace_without_existing_block_discards_file() {
        let out = merge("# Old\n\nlots of prior content\n", "body", Replace, ID);
        assert_eq!(out, format!("{}\n", block(ID, "body")));
    }

    #[test]
    fn reinstall_updates_block_in_place() {
        let first = merge("# Title\n", "v1", Append, ID);
        let second = merge(&first, "v2", Append, ID);
        assert_eq!(second, format!("# Title\n\n{}\n", block(ID, "v2")));
    }

    #[test]
    fn reinstall_with_same_content_is_idempotent() {
        let once = merge("# Title\n", "body", Append, ID);
        let twice = merge(&once, "body", Append, ID);
        assert_eq!(once, twice);
    }

    #[test]
    fn strategy_is_ignored_when_block_already_present() {
        let base = merge("# Title\n", "body", Append, ID);
        // Re-run with Prepend: block stays where it was (in place), not moved to top.
        let out = merge(&base, "body", Prepend, ID);
        assert_eq!(out, base);
    }

    #[test]
    fn different_ids_do_not_collide() {
        let a = merge("", "first", Append, "alpha");
        let ab = merge(&a, "second", Append, "bravo");
        assert!(ab.contains("<!-- BEGIN aimgr: alpha -->"));
        assert!(ab.contains("<!-- BEGIN aimgr: bravo -->"));
        assert!(ab.find("alpha").unwrap() < ab.find("bravo").unwrap());
    }

    #[test]
    fn append_preserves_install_order() {
        let mut out = String::from("# Title\n");
        out = merge(&out, "one", Append, "alpha");
        out = merge(&out, "two", Append, "bravo");
        out = merge(&out, "three", Append, "charlie");
        let positions = ["alpha", "bravo", "charlie"].map(|id| out.find(id).unwrap());
        assert!(positions[0] < positions[1] && positions[1] < positions[2]);
    }

    #[test]
    fn render_block_matches_what_merge_writes() {
        let out = merge("", "body", Append, ID);
        let block = render_block(ID, "body");
        assert!(out.contains(&block));
    }

    #[test]
    fn extract_block_roundtrips_pristine() {
        let out = merge("# Title\n", "body", Append, ID);
        let block = render_block(ID, "body");
        assert_eq!(extract_block(&out, ID), Some(block.as_str()));
    }

    #[test]
    fn extract_block_returns_none_when_missing() {
        assert_eq!(extract_block("# Title\n", ID), None);
    }

    #[test]
    fn extract_body_returns_inner_content_only() {
        let out = merge("# Title\n", "hand-edited body", Append, ID);
        assert_eq!(extract_body(&out, ID), Some("hand-edited body"));
    }

    #[test]
    fn extract_body_returns_none_when_missing() {
        assert_eq!(extract_body("# Title\n", ID), None);
    }

    #[test]
    fn split_body_without_custom_markers_yields_managed_only() {
        let (managed, custom) = split_body("plain body\nmore", ID);
        assert_eq!(managed, "plain body\nmore");
        assert!(custom.is_none());
    }

    #[test]
    fn split_body_detects_bottom_custom_region() {
        let body = format!(
            "managed body\n\n<!-- BEGIN aimgr-custom: {ID} -->\nmy edits\n<!-- END aimgr-custom: {ID} -->"
        );
        let (managed, custom) = split_body(&body, ID);
        assert_eq!(managed, "managed body");
        let (pos, content) = custom.unwrap();
        assert_eq!(pos, CustomPosition::Bottom);
        assert_eq!(content, "my edits");
    }

    #[test]
    fn split_body_detects_top_custom_region() {
        let body = format!(
            "<!-- BEGIN aimgr-custom: {ID} -->\nmy edits\n<!-- END aimgr-custom: {ID} -->\n\nmanaged body"
        );
        let (managed, custom) = split_body(&body, ID);
        assert_eq!(managed, "managed body");
        let (pos, content) = custom.unwrap();
        assert_eq!(pos, CustomPosition::Top);
        assert_eq!(content, "my edits");
    }

    #[test]
    fn compose_body_roundtrips_through_split() {
        let composed = compose_body("managed", Some((CustomPosition::Bottom, "mine")), ID);
        let (managed, custom) = split_body(&composed, ID);
        assert_eq!(managed, "managed");
        assert_eq!(
            custom.unwrap(),
            (CustomPosition::Bottom, "mine".to_string())
        );
    }

    #[test]
    fn compose_body_without_custom_returns_managed_only() {
        assert_eq!(compose_body("managed\n", None, ID), "managed");
    }

    #[test]
    fn remove_block_at_start() {
        let dest = format!("{}\n\n# rest\n", block(ID, "body"));
        assert_eq!(remove(&dest, ID), "# rest\n");
    }

    #[test]
    fn remove_block_at_end() {
        let dest = format!("# top\n\n{}\n", block(ID, "body"));
        assert_eq!(remove(&dest, ID), "# top\n");
    }

    #[test]
    fn remove_block_between_content_collapses_to_single_blank_line() {
        let dest = format!("# top\n\n{}\n\n# bottom\n", block(ID, "body"));
        assert_eq!(remove(&dest, ID), "# top\n\n# bottom\n");
    }

    #[test]
    fn remove_block_alone_yields_empty() {
        let dest = format!("{}\n", block(ID, "body"));
        assert_eq!(remove(&dest, ID), "");
    }

    #[test]
    fn remove_missing_block_is_noop() {
        let dest = "# Title\n\nnothing to remove\n";
        assert_eq!(remove(dest, ID), dest);
    }

    #[test]
    fn install_then_uninstall_round_trip_for_append() {
        let dest = "# Title\n\nbody\n";
        let after_install = merge(dest, "extra", Append, ID);
        let after_uninstall = remove(&after_install, ID);
        assert_eq!(after_uninstall, dest);
    }

    #[test]
    fn block_content_with_leading_or_trailing_newlines_is_normalized() {
        let out = merge("", "\n\nbody\n\n", Append, ID);
        assert_eq!(out, format!("{}\n", block(ID, "body")));
    }
}
