use crate::error::EditorError;
use crate::model::block::{Block, BlockAlignment, BlockId};
use crate::model::document::Document;
use crate::model::inline::{
    Mark, TextSpan, next_char_boundary_utf16, normalize_spans, prev_char_boundary_utf16,
    utf16_to_byte,
};
use crate::model::selection::Selection;
use crate::op::operation::Operation;

/// Apply a single operation to the document. This is the ONLY place that mutates Document state.
pub fn apply_op(
    doc: &mut Document,
    selection: &mut Option<Selection>,
    op: &Operation,
) -> Result<(), EditorError> {
    match op {
        Operation::InsertTextAt {
            block_id,
            offset,
            text,
            marks,
        } => apply_insert_text(doc, block_id, *offset, text, marks),

        Operation::DeleteRange {
            block_id,
            start,
            end,
        } => apply_delete_range(doc, block_id, *start, *end),

        Operation::SplitBlock {
            block_id,
            offset,
            new_block_id,
        } => apply_split_block(doc, block_id, *offset, new_block_id),

        Operation::MergeBlock {
            target_id,
            source_id,
        } => apply_merge_block(doc, target_id, source_id),

        Operation::SetBlockType {
            block_id,
            block_type,
            props,
        } => {
            doc.set_block_type(block_id, block_type.clone(), props.clone())
                .ok_or_else(|| EditorError::BlockNotFound(block_id.clone()))?;
            Ok(())
        }

        Operation::SetBlockAlign { block_id, align } => {
            let block = doc.require_block_mut(block_id)?;
            // `Left` is the default — clear the prop instead of storing it so
            // the document stays minimal and round-trips cleanly through JSON.
            if *align == BlockAlignment::Left {
                block.props.remove("align");
            } else {
                block.props.insert(
                    "align".into(),
                    serde_json::Value::String(align.as_str().into()),
                );
            }
            Ok(())
        }

        Operation::ToggleMark {
            block_id,
            start,
            end,
            mark_type,
        } => apply_toggle_mark(doc, block_id, *start, *end, mark_type),

        Operation::SetMark {
            block_id,
            start,
            end,
            mark,
        } => apply_set_mark(doc, block_id, *start, *end, mark),

        Operation::ClearMark {
            block_id,
            start,
            end,
            mark_type,
        } => apply_clear_mark(doc, block_id, *start, *end, mark_type),

        Operation::RestoreRangeSpans {
            block_id,
            start,
            end,
            spans,
        } => apply_restore_range_spans(doc, block_id, *start, *end, spans),

        Operation::InsertBlock { block, index } => {
            doc.insert_block(block.clone(), *index);
            Ok(())
        }

        Operation::RemoveBlock { block_id } => {
            doc.remove_block(block_id)
                .ok_or(EditorError::BlockNotFound(block_id.clone()))?;
            Ok(())
        }

        Operation::MoveBlock {
            block_id,
            new_index,
        } => {
            let root = doc.root_mut();
            let old_idx = root
                .iter()
                .position(|id| id == block_id)
                .ok_or_else(|| EditorError::BlockNotFound(block_id.clone()))?;
            let id = root.remove(old_idx);
            let insert_idx = (*new_index).min(root.len());
            root.insert(insert_idx, id);
            Ok(())
        }

        Operation::SetSelection { selection: new_sel } => {
            *selection = new_sel.clone();
            Ok(())
        }

        // --- Table / nested block operations ---
        Operation::InsertChildBlock {
            parent_id,
            block,
            index,
        } => doc.insert_child_block(parent_id, block.clone(), *index),

        Operation::RemoveChildBlock {
            parent_id,
            block_id,
        } => {
            doc.remove_child_block(parent_id, block_id)?;
            Ok(())
        }

        Operation::SetBlockProps {
            block_id, props, ..
        } => {
            let block = doc.require_block_mut(block_id)?;
            for (key, value) in props {
                block.props.insert(key.clone(), value.clone());
            }
            Ok(())
        }
    }
}

fn clamp_utf16_offset(block: &Block, offset: usize) -> usize {
    let text = block.plain_text();
    prev_char_boundary_utf16(&text, offset.min(block.text_len()))
}

fn clamp_utf16_range(block: &Block, start: usize, end: usize) -> (usize, usize) {
    let text = block.plain_text();
    let total_len = block.text_len();
    let start = prev_char_boundary_utf16(&text, start.min(total_len));
    let end = next_char_boundary_utf16(&text, end.min(total_len));
    (start, end)
}

fn apply_insert_text(
    doc: &mut Document,
    block_id: &BlockId,
    offset: usize,
    text: &str,
    marks: &[Mark],
) -> Result<(), EditorError> {
    let block = doc.require_block_mut(block_id)?;

    let total_len = block.text_len();
    if offset > total_len {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset,
            len: total_len,
        });
    }

    let offset = clamp_utf16_offset(block, offset);

    // Find the span and local offset to insert at
    let mut cumulative = 0;
    let mut insert_idx = block.content.len();
    let mut local_offset = 0;

    for (i, span) in block.content.iter().enumerate() {
        if cumulative + span.len() >= offset {
            insert_idx = i;
            local_offset = offset - cumulative;
            break;
        }
        cumulative += span.len();
    }

    let marks_vec = marks.to_vec();

    if block.content.is_empty() {
        block
            .content
            .push(TextSpan::with_marks(text.to_string(), marks_vec));
    } else if insert_idx < block.content.len() {
        let span = &block.content[insert_idx];
        if span.marks == marks_vec {
            // Same marks, insert directly into this span
            let mut new_text = span.text.clone();
            let byte_offset = utf16_to_byte(&new_text, local_offset);
            new_text.insert_str(byte_offset, text);
            block.content[insert_idx] = TextSpan::with_marks(new_text, marks_vec);
        } else {
            // Different marks, split the span and insert new one in between
            let existing = &block.content[insert_idx];
            let byte_offset = utf16_to_byte(&existing.text, local_offset);
            let before = existing.text[..byte_offset].to_string();
            let after = existing.text[byte_offset..].to_string();
            let existing_marks = existing.marks.clone();

            let mut new_spans = Vec::new();
            if !before.is_empty() {
                new_spans.push(TextSpan::with_marks(before, existing_marks.clone()));
            }
            new_spans.push(TextSpan::with_marks(text.to_string(), marks_vec));
            if !after.is_empty() {
                new_spans.push(TextSpan::with_marks(after, existing_marks));
            }

            block.content.splice(insert_idx..=insert_idx, new_spans);
        }
    } else {
        block
            .content
            .push(TextSpan::with_marks(text.to_string(), marks_vec));
    }

    normalize_spans(&mut block.content);
    Ok(())
}

fn apply_delete_range(
    doc: &mut Document,
    block_id: &BlockId,
    start: usize,
    end: usize,
) -> Result<(), EditorError> {
    let block = doc.require_block_mut(block_id)?;

    let total_len = block.text_len();
    if end > total_len || start > end {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset: end,
            len: total_len,
        });
    }

    if start == end {
        return Ok(());
    }

    let (start, end) = clamp_utf16_range(block, start, end);
    if start == end {
        return Ok(());
    }

    let mut new_spans = Vec::new();
    let mut cumulative = 0;

    for span in &block.content {
        let span_start = cumulative;
        let span_end = cumulative + span.len();
        cumulative = span_end;

        if span_end <= start || span_start >= end {
            new_spans.push(span.clone());
        } else {
            let local_start = start.saturating_sub(span_start);
            let local_end = (end - span_start).min(span.len());
            let byte_start = utf16_to_byte(&span.text, local_start);
            let byte_end = utf16_to_byte(&span.text, local_end);

            let keep_before = if span_start < start {
                Some(&span.text[..byte_start])
            } else {
                None
            };
            let keep_after = if span_end > end {
                Some(&span.text[byte_end..])
            } else {
                None
            };

            if let Some(before) = keep_before
                && !before.is_empty()
            {
                new_spans.push(TextSpan::with_marks(before.to_string(), span.marks.clone()));
            }
            if let Some(after) = keep_after
                && !after.is_empty()
            {
                new_spans.push(TextSpan::with_marks(after.to_string(), span.marks.clone()));
            }
        }
    }

    block.content = new_spans;
    normalize_spans(&mut block.content);
    Ok(())
}

fn apply_split_block(
    doc: &mut Document,
    block_id: &BlockId,
    offset: usize,
    new_block_id: &BlockId,
) -> Result<(), EditorError> {
    let block = doc.require_block(block_id)?;

    let total_len = block.text_len();
    if offset > total_len {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset,
            len: total_len,
        });
    }

    let offset = clamp_utf16_offset(block, offset);
    let (before_spans, after_spans) = split_spans_at(&block.content, offset);
    let block_type = block.ty.clone();
    let block_props = block.props.clone();
    let block_parent = block.parent.clone();

    let block_index = doc
        .block_root_index(block_id)
        .ok_or_else(|| EditorError::BlockNotFound(block_id.clone()))?;

    let original = doc.require_block_mut(block_id)?;
    original.content = before_spans;

    let mut new_block = crate::model::block::Block {
        id: new_block_id.clone(),
        parent: block_parent,
        children: Vec::new(),
        ty: block_type,
        props: block_props,
        content: after_spans,
    };

    normalize_spans(&mut new_block.content);
    doc.insert_block(new_block, block_index + 1);
    doc.normalize_block(block_id);

    Ok(())
}

fn apply_merge_block(
    doc: &mut Document,
    target_id: &BlockId,
    source_id: &BlockId,
) -> Result<(), EditorError> {
    let source = doc.require_block(source_id)?;
    let source_content = source.content.clone();

    let target = doc.require_block_mut(target_id)?;
    target.content.extend(source_content);
    normalize_spans(&mut target.content);

    doc.remove_block(source_id);
    Ok(())
}

fn apply_toggle_mark(
    doc: &mut Document,
    block_id: &BlockId,
    start: usize,
    end: usize,
    mark_type: &str,
) -> Result<(), EditorError> {
    if start >= end {
        return Ok(());
    }

    let block = doc.require_block(block_id)?;

    let total_len = block.text_len();
    if end > total_len {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset: end,
            len: total_len,
        });
    }

    let (start, end) = clamp_utf16_range(block, start, end);
    if start >= end {
        return Ok(());
    }

    let all_marked = crate::model::inline::all_have_mark(&block.content, start, end, mark_type);

    let block = doc.require_block_mut(block_id)?;

    let mut new_spans = Vec::new();
    let mut cumulative = 0;

    for span in &block.content {
        let span_start = cumulative;
        let span_end = cumulative + span.len();
        cumulative = span_end;

        if span_end <= start || span_start >= end {
            new_spans.push(span.clone());
        } else {
            let range_start_in_span = start.saturating_sub(span_start);
            let range_end_in_span = (end - span_start).min(span.len());
            let byte_range_start = utf16_to_byte(&span.text, range_start_in_span);
            let byte_range_end = utf16_to_byte(&span.text, range_end_in_span);

            // Before the range
            if range_start_in_span > 0 {
                new_spans.push(TextSpan::with_marks(
                    span.text[..byte_range_start].to_string(),
                    span.marks.clone(),
                ));
            }

            // In the range — toggle the mark
            let mut toggled_marks = span.marks.clone();
            if all_marked {
                // Remove the mark
                toggled_marks.retain(|m| m.ty != mark_type);
            } else if !span.has_mark(mark_type) {
                // Add the mark
                toggled_marks.push(Mark::new(mark_type));
            }

            new_spans.push(TextSpan::with_marks(
                span.text[byte_range_start..byte_range_end].to_string(),
                toggled_marks,
            ));

            // After the range
            if range_end_in_span < span.len() {
                new_spans.push(TextSpan::with_marks(
                    span.text[byte_range_end..].to_string(),
                    span.marks.clone(),
                ));
            }
        }
    }

    block.content = new_spans;
    normalize_spans(&mut block.content);
    Ok(())
}

fn apply_set_mark(
    doc: &mut Document,
    block_id: &BlockId,
    start: usize,
    end: usize,
    mark: &Mark,
) -> Result<(), EditorError> {
    if start >= end {
        return Ok(());
    }

    let block = doc.require_block(block_id)?;

    let total_len = block.text_len();
    if end > total_len {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset: end,
            len: total_len,
        });
    }

    let (start, end) = clamp_utf16_range(block, start, end);
    if start >= end {
        return Ok(());
    }

    let block = doc.require_block_mut(block_id)?;

    let mut new_spans = Vec::new();
    let mut cumulative = 0;

    for span in &block.content {
        let span_start = cumulative;
        let span_end = cumulative + span.len();
        cumulative = span_end;

        if span_end <= start || span_start >= end {
            new_spans.push(span.clone());
        } else {
            let range_start_in_span = start.saturating_sub(span_start);
            let range_end_in_span = (end - span_start).min(span.len());
            let byte_range_start = utf16_to_byte(&span.text, range_start_in_span);
            let byte_range_end = utf16_to_byte(&span.text, range_end_in_span);

            if range_start_in_span > 0 {
                new_spans.push(TextSpan::with_marks(
                    span.text[..byte_range_start].to_string(),
                    span.marks.clone(),
                ));
            }

            let mut updated_marks = span.marks.clone();
            updated_marks.retain(|m| m.ty != mark.ty);
            updated_marks.push(mark.clone());

            new_spans.push(TextSpan::with_marks(
                span.text[byte_range_start..byte_range_end].to_string(),
                updated_marks,
            ));

            if range_end_in_span < span.len() {
                new_spans.push(TextSpan::with_marks(
                    span.text[byte_range_end..].to_string(),
                    span.marks.clone(),
                ));
            }
        }
    }

    block.content = new_spans;
    normalize_spans(&mut block.content);
    Ok(())
}

fn apply_clear_mark(
    doc: &mut Document,
    block_id: &BlockId,
    start: usize,
    end: usize,
    mark_type: &str,
) -> Result<(), EditorError> {
    if start >= end {
        return Ok(());
    }

    let block = doc.require_block(block_id)?;

    let total_len = block.text_len();
    if end > total_len {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset: end,
            len: total_len,
        });
    }

    let (start, end) = clamp_utf16_range(block, start, end);
    if start >= end {
        return Ok(());
    }

    let block = doc.require_block_mut(block_id)?;

    let mut new_spans = Vec::new();
    let mut cumulative = 0;

    for span in &block.content {
        let span_start = cumulative;
        let span_end = cumulative + span.len();
        cumulative = span_end;

        if span_end <= start || span_start >= end {
            new_spans.push(span.clone());
        } else {
            let range_start_in_span = start.saturating_sub(span_start);
            let range_end_in_span = (end - span_start).min(span.len());
            let byte_range_start = utf16_to_byte(&span.text, range_start_in_span);
            let byte_range_end = utf16_to_byte(&span.text, range_end_in_span);

            if range_start_in_span > 0 {
                new_spans.push(TextSpan::with_marks(
                    span.text[..byte_range_start].to_string(),
                    span.marks.clone(),
                ));
            }

            let mut updated_marks = span.marks.clone();
            updated_marks.retain(|m| m.ty != mark_type);

            new_spans.push(TextSpan::with_marks(
                span.text[byte_range_start..byte_range_end].to_string(),
                updated_marks,
            ));

            if range_end_in_span < span.len() {
                new_spans.push(TextSpan::with_marks(
                    span.text[byte_range_end..].to_string(),
                    span.marks.clone(),
                ));
            }
        }
    }

    block.content = new_spans;
    normalize_spans(&mut block.content);
    Ok(())
}

fn apply_restore_range_spans(
    doc: &mut Document,
    block_id: &BlockId,
    start: usize,
    end: usize,
    spans: &[TextSpan],
) -> Result<(), EditorError> {
    let block = doc.require_block(block_id)?;

    let total_len = block.text_len();
    if end > total_len {
        return Err(EditorError::OffsetOutOfRange {
            block_id: block_id.clone(),
            offset: end,
            len: total_len,
        });
    }

    let (start, end) = clamp_utf16_range(block, start, end);

    let block = doc.require_block_mut(block_id)?;

    let mut new_spans = Vec::new();
    let mut cumulative = 0;
    let mut inserted = false;

    for span in &block.content {
        let span_start = cumulative;
        let span_end = cumulative + span.len();
        cumulative = span_end;

        if span_end <= start || span_start >= end {
            if !inserted && span_start >= end {
                new_spans.extend(spans.iter().cloned());
                inserted = true;
            }
            new_spans.push(span.clone());
            continue;
        }

        let local_start = start.saturating_sub(span_start);
        let local_end = (end - span_start).min(span.len());
        let byte_start = utf16_to_byte(&span.text, local_start);
        let byte_end = utf16_to_byte(&span.text, local_end);

        if local_start > 0 {
            new_spans.push(TextSpan::with_marks(
                span.text[..byte_start].to_string(),
                span.marks.clone(),
            ));
        }

        if !inserted {
            new_spans.extend(spans.iter().cloned());
            inserted = true;
        }

        if local_end < span.len() {
            new_spans.push(TextSpan::with_marks(
                span.text[byte_end..].to_string(),
                span.marks.clone(),
            ));
        }
    }

    if !inserted {
        new_spans.extend(spans.iter().cloned());
    }

    block.content = new_spans;
    normalize_spans(&mut block.content);
    Ok(())
}

/// Split spans at a UTF-16 code unit offset, returning (before, after).
fn split_spans_at(spans: &[TextSpan], offset: usize) -> (Vec<TextSpan>, Vec<TextSpan>) {
    let mut before = Vec::new();
    let mut after = Vec::new();
    let mut cumulative = 0;
    let mut split_done = false;

    for span in spans {
        if split_done {
            after.push(span.clone());
            continue;
        }

        let span_end = cumulative + span.len();

        if span_end <= offset {
            before.push(span.clone());
        } else if cumulative >= offset {
            after.push(span.clone());
            split_done = true;
        } else {
            let local = offset - cumulative;
            let byte_local = utf16_to_byte(&span.text, local);
            let before_text = &span.text[..byte_local];
            let after_text = &span.text[byte_local..];

            if !before_text.is_empty() {
                before.push(TextSpan::with_marks(
                    before_text.to_string(),
                    span.marks.clone(),
                ));
            }
            if !after_text.is_empty() {
                after.push(TextSpan::with_marks(
                    after_text.to_string(),
                    span.marks.clone(),
                ));
            }
            split_done = true;
        }

        cumulative = span_end;
    }

    (before, after)
}
