use std::borrow::Cow;

use super::*;

#[derive(Debug, Clone, Copy)]
pub(super) enum HeredocTailTextMode {
    Rendered,
    Assignment,
}

enum RenderedFragment<'text> {
    Plain(Cow<'text, str>),
    RenderedShellText(Cow<'text, str>),
    PreserveCurrentIndent(Cow<'text, str>),
    AssignmentCommandSubstitution(Cow<'text, str>),
    HeredocTail {
        text: Cow<'text, str>,
        assignment_context: bool,
    },
}

pub(super) fn assignment_contains_command_heredoc(
    assignment: &Assignment,
    facts: &FormatterFacts<'_>,
) -> bool {
    match &assignment.value {
        AssignmentValue::Scalar(word) => word_contains_command_heredoc(word, facts),
        AssignmentValue::Compound(array) => array
            .elements
            .iter()
            .any(|element| word_contains_command_heredoc(array_elem_parts(element).1, facts)),
    }
}

pub(super) fn compound_assignment_is_single_case_command_substitution(
    assignment: &Assignment,
) -> bool {
    let AssignmentValue::Compound(array) = &assignment.value else {
        return false;
    };
    let [ArrayElem::Sequential(word)] = array.elements.as_slice() else {
        return false;
    };
    let [part] = word.parts.as_slice() else {
        return false;
    };
    let WordPart::CommandSubstitution { body, .. } = &part.kind else {
        return false;
    };
    matches!(
        body.as_slice(),
        [stmt]
            if !stmt.negated
                && stmt.redirects.is_empty()
                && matches!(&stmt.command, Command::Compound(CompoundCommand::Case(_)))
    )
}

pub(super) fn word_contains_command_heredoc(word: &Word, facts: &FormatterFacts<'_>) -> bool {
    word.parts
        .iter()
        .any(|part| word_part_contains_command_heredoc(&part.kind, facts))
}

pub(super) fn heredoc_body_contains_command_substitution(body: &HeredocBody) -> bool {
    body.parts
        .iter()
        .any(|part| matches!(part.kind, HeredocBodyPart::CommandSubstitution { .. }))
}

fn word_part_contains_command_heredoc(part: &WordPart, facts: &FormatterFacts<'_>) -> bool {
    match part {
        WordPart::CommandSubstitution { body, .. } | WordPart::ProcessSubstitution { body, .. } => {
            facts.sequence_contains_heredoc(body)
        }
        WordPart::DoubleQuoted { parts, .. } => parts
            .iter()
            .any(|part| word_part_contains_command_heredoc(&part.kind, facts)),
        _ => false,
    }
}

pub(super) fn normalize_escaped_multiline_word_command_substitution_indent(
    rendered: &str,
    options: &ResolvedShellFormatOptions,
) -> Option<String> {
    let normalized = rendered.strip_prefix('$').unwrap_or(rendered);
    if !normalized.starts_with("\"\\\n") && !normalized.starts_with("\"\\\r\n") {
        return None;
    }

    let indent = options.indent_prefix(1);
    let mut output = String::with_capacity(rendered.len() + indent.len() * 4);
    let mut changed = false;
    let mut command_substitution_depth = 0usize;

    for (index, line) in rendered.split('\n').enumerate() {
        if index > 0 {
            output.push('\n');
        }

        let trimmed = line.trim_start_matches([' ', '\t']);
        if command_substitution_depth > 0 {
            if !line.is_empty() {
                output.push_str(&indent);
                changed = true;
            }
            output.push_str(line);
            if trimmed.starts_with(')') {
                command_substitution_depth = command_substitution_depth.saturating_sub(1);
            }
            if trimmed.ends_with("$(") {
                command_substitution_depth += 1;
            }
            continue;
        }

        output.push_str(line);
        if trimmed.ends_with("$(") {
            command_substitution_depth = 1;
        }
    }

    changed.then_some(output)
}

pub(super) fn normalize_rendered_leading_list_operator_continuations(
    rendered: &str,
) -> Option<String> {
    let mut output = Vec::<String>::new();
    let mut changed = false;

    for line in rendered.split('\n') {
        let mut current = line.to_string();
        if let Some((operator, rest)) = leading_list_operator_line_parts(line)
            && let Some(previous) = output.last_mut()
            && let Some(prefix_len) = line_without_continuation_backslash(previous).map(str::len)
        {
            previous.truncate(prefix_len);
            previous.push(' ');
            previous.push_str(operator);
            current.clear();
            current.push_str(rest);
            changed = true;
        }
        output.push(current);
    }

    changed.then(|| output.join("\n"))
}

fn leading_list_operator_line_parts(line: &str) -> Option<(&'static str, &str)> {
    let trimmed = line.trim_start_matches([' ', '\t', '\r']);
    let (operator, rest) = if let Some(rest) = trimmed.strip_prefix("||") {
        ("||", rest)
    } else if let Some(rest) = trimmed.strip_prefix("&&") {
        ("&&", rest)
    } else if let Some(rest) = trimmed.strip_prefix("|&") {
        ("|&", rest)
    } else if let Some(rest) = trimmed.strip_prefix('|') {
        if trimmed.starts_with("|)") {
            return None;
        }
        ("|", rest)
    } else {
        return None;
    };

    Some((operator, rest.trim_start_matches([' ', '\t', '\r'])))
}

pub(super) fn normalize_scalar_assignment_unquoted_continuations(
    assignment: &Assignment,
    source: &str,
    facts: &FormatterFacts,
) -> Option<String> {
    if assignment_source_has_command_substitution(assignment, source) {
        return None;
    }
    let AssignmentValue::Scalar(_) = &assignment.value else {
        return None;
    };
    if !facts.has_raw_continuation_backslash_between(
        assignment.span.start.offset(),
        assignment.span.end.offset(),
    ) {
        return None;
    }

    let raw = assignment.span.slice(source);
    let mut head = String::new();
    render_assignment_head_to_buf(assignment, source, &mut head);
    let raw_value = raw.strip_prefix(&head)?;
    let normalized_value = normalize_raw_unquoted_word_continuations(raw_value)?;
    let mut normalized = head;
    normalized.push_str(&normalized_value);
    Some(normalized)
}

pub(super) fn assignment_has_quoted_backslash_continuation_literal(
    assignment: &Assignment,
    source: &str,
) -> bool {
    let AssignmentValue::Scalar(_) = &assignment.value else {
        return false;
    };
    let raw = assignment.span.slice(source);
    raw.contains("\\\n")
        && RawShellText::new(raw).quoted_backslash_continuation()
        && !raw.contains("$(")
        && !raw.contains('`')
        && !raw.contains("<(")
        && !raw.contains(">(")
}

pub(super) fn assignment_source_has_command_substitution(
    assignment: &Assignment,
    source: &str,
) -> bool {
    let raw = assignment.span.slice(source);
    raw.contains("$(") || raw.contains('`') || raw.contains("<(") || raw.contains(">(")
}

pub(super) fn assignment_source_has_leading_pipe_continuation(
    assignment: &Assignment,
    source: &str,
) -> bool {
    let raw = assignment.span.slice(source);
    let mut rest = raw;
    while let Some(index) = rest.find("\\\n") {
        let after_break = &rest[index + 2..];
        let trimmed = after_break.trim_start_matches([' ', '\t', '\r']);
        if trimmed.starts_with('|') && !trimmed.starts_with("||") {
            return true;
        }
        rest = after_break;
    }
    false
}

pub(super) fn assignment_value_is_quoted_formattable_command_substitution_only(
    assignment: &Assignment,
    facts: &FormatterFacts<'_>,
) -> bool {
    match &assignment.value {
        AssignmentValue::Scalar(word) => {
            word_is_quoted_formattable_command_substitution_only_with_facts(word, facts)
        }
        AssignmentValue::Compound(_) => false,
    }
}

pub(super) fn assignment_value_is_quoted_command_substitution_only(
    assignment: &Assignment,
) -> bool {
    match &assignment.value {
        AssignmentValue::Scalar(word) => word_is_quoted_command_substitution_only(word),
        AssignmentValue::Compound(_) => false,
    }
}

#[derive(Clone, Copy)]
enum MultilineLiteralQuote {
    Single,
    Double,
}

fn multiline_literal_quote_state_after_line(
    line: &str,
    mut quote: Option<MultilineLiteralQuote>,
) -> Option<MultilineLiteralQuote> {
    let mut escaped = false;
    for ch in line.chars() {
        if escaped {
            escaped = false;
            continue;
        }
        match quote {
            Some(MultilineLiteralQuote::Single) => {
                if ch == '\'' {
                    quote = None;
                }
            }
            Some(MultilineLiteralQuote::Double) => {
                if ch == '\\' {
                    escaped = true;
                } else if ch == '"' {
                    quote = None;
                }
            }
            None => {
                if ch == '\'' {
                    quote = Some(MultilineLiteralQuote::Single);
                } else if ch == '"' {
                    quote = Some(MultilineLiteralQuote::Double);
                } else if ch == '\\' {
                    escaped = true;
                }
            }
        }
    }
    quote
}

pub(super) fn pipeline_operator_breaks(
    statements: &[&Stmt],
    operators: &[(BinaryOp, Span)],
    source: &str,
    source_map: &SourceMap<'_>,
) -> Vec<bool> {
    let mut breaks = Vec::with_capacity(operators.len());
    for (statement, (_, operator_span)) in statements.iter().skip(1).zip(operators.iter()) {
        let next_start =
            interstitial_comment_end(statement, operator_span.end.offset(), source, source_map);
        breaks.push(
            source_map.operator_starts_or_ends_line(*operator_span)
                || source_map.contains_newline_between(operator_span.end.offset(), next_start),
        );
    }

    breaks
}

fn command_substitution_assignment_line_needs_context_indent(
    remaining: &str,
    options: &ResolvedShellFormatOptions,
) -> bool {
    match options.indent_style() {
        IndentStyle::Tab => !remaining.starts_with(' '),
        IndentStyle::Space => true,
    }
}

fn command_substitution_assignment_line_closes_block(remaining: &str) -> bool {
    remaining
        .lines()
        .next()
        .is_some_and(|line| line.trim_start_matches([' ', '\t']).starts_with(')'))
}

pub(super) fn interstitial_comment_end(
    stmt: &Stmt,
    operator_end: usize,
    source: &str,
    source_map: &SourceMap<'_>,
) -> usize {
    stmt_start_after_operator(stmt, operator_end, source, source_map)
}

fn emitted_line_indent_column(
    line: &str,
    pipeline_indent_column: Option<usize>,
    add_context_indent: bool,
    base_indent_column: usize,
    options: &ResolvedShellFormatOptions,
) -> usize {
    pipeline_indent_column.unwrap_or_else(|| {
        let line_indent = rendered_line_indent_column(line, options);
        if add_context_indent {
            base_indent_column + line_indent
        } else {
            line_indent
        }
    })
}

fn rendered_line_indent_column(line: &str, options: &ResolvedShellFormatOptions) -> usize {
    let mut column = 0;
    for ch in line.chars() {
        match ch {
            '\t' if matches!(options.indent_style(), IndentStyle::Tab) => column += 1,
            ' ' => column += 1,
            _ => break,
        }
    }
    column
}

fn rendered_line_with_indent_column(
    line: &str,
    column: usize,
    options: &ResolvedShellFormatOptions,
) -> String {
    let content = line.trim_start_matches([' ', '\t']);
    let mut rendered = String::with_capacity(line.len());
    options.push_indent_columns(&mut rendered, column);
    rendered.push_str(content);
    rendered
}

fn command_substitution_shell_text_indent_column(
    line: &str,
    line_starts_in_quote: bool,
    emitted_indent_column: usize,
    base_indent_column: usize,
    indent_unit: usize,
) -> Option<usize> {
    if line_starts_in_quote {
        return None;
    }
    let content = line.trim_end_matches(['\r', '\n']);
    let scan_start = command_substitution_context_start(content).unwrap_or(0);
    let trimmed = content[scan_start..].trim_start_matches([' ', '\t']);
    if trimmed.is_empty() || trimmed.starts_with('#') {
        return None;
    }
    if inline_assignment_command_substitution_context(content, scan_start) {
        Some(base_indent_column + indent_unit)
    } else {
        Some(emitted_indent_column)
    }
}

fn inline_assignment_command_substitution_context(content: &str, scan_start: usize) -> bool {
    if scan_start == 0 {
        return false;
    }
    let prefix = content[..scan_start.saturating_sub(2)].trim_end_matches([' ', '\t']);
    prefix.ends_with('"') && prefix.contains('=')
}

fn next_command_substitution_pipeline_indent_column(
    continuation: CommandSubstitutionPipelineContinuation,
    starts_with_block_command_substitution: bool,
    inline_pipeline_indent_column: usize,
    active_shell_pipeline_indent_column: Option<usize>,
    active_shell_line_was_pipeline_stage: bool,
    indent_unit: usize,
    current_pipeline_indent_column: Option<usize>,
) -> Option<usize> {
    match continuation {
        CommandSubstitutionPipelineContinuation::None => None,
        CommandSubstitutionPipelineContinuation::Comment => current_pipeline_indent_column,
        CommandSubstitutionPipelineContinuation::StructuralPipe {
            line_started_in_quote,
        } => {
            if !starts_with_block_command_substitution {
                Some(inline_pipeline_indent_column)
            } else if line_started_in_quote && active_shell_line_was_pipeline_stage {
                active_shell_pipeline_indent_column
            } else if line_started_in_quote {
                active_shell_pipeline_indent_column.map(|column| column + indent_unit)
            } else {
                None
            }
        }
    }
}

fn strip_assignment_context_indent<'a>(
    line: &'a str,
    base_indent_column: usize,
    options: &ResolvedShellFormatOptions,
) -> &'a str {
    if base_indent_column == 0 {
        return line;
    }

    match options.indent_style() {
        IndentStyle::Tab => {
            let leading_tabs = line.bytes().take_while(|byte| *byte == b'\t').count();
            if leading_tabs <= base_indent_column {
                line
            } else {
                &line[base_indent_column..]
            }
        }
        IndentStyle::Space => {
            let leading_spaces = line.bytes().take_while(|byte| *byte == b' ').count();
            if leading_spaces <= base_indent_column {
                line
            } else {
                &line[base_indent_column..]
            }
        }
    }
}

pub(super) fn normalize_literal_assignment_command_substitution_pipelines(
    text: &str,
    continuation_indent: &str,
) -> String {
    let mut output = String::with_capacity(text.len());
    let mut indent_next = false;
    let mut changed = false;
    let mut rest = text;

    while !rest.is_empty() {
        let (line, next, had_newline) = split_first_line(rest);

        let trimmed_start = line.trim_start_matches([' ', '\t']);
        let is_continuation_comment = indent_next && trimmed_start.starts_with('#');
        let indent_line = indent_next && !line.trim_matches([' ', '\t', '\r']).is_empty();

        if indent_line {
            output.push_str(continuation_indent);
            output.push_str(trimmed_start);
            changed |= !line.starts_with(continuation_indent)
                || line[continuation_indent.len()..].starts_with([' ', '\t']);
        } else {
            output.push_str(line);
        }

        if had_newline {
            output.push('\n');
        }

        let line_to_check = if indent_line { trimmed_start } else { line };
        indent_next = if is_continuation_comment {
            true
        } else if indent_next {
            rendered_line_ends_with_structural_pipe_continuation(line_to_check)
        } else {
            rendered_line_opens_command_substitution_pipeline(line_to_check)
        };

        rest = next;
    }

    if changed { output } else { text.to_string() }
}

pub(super) fn conditional_binary_has_explicit_rhs_break(
    expression: &ConditionalBinaryExpr,
    source_map: &SourceMap<'_>,
) -> bool {
    if !matches!(
        expression.op,
        ConditionalBinaryOp::And | ConditionalBinaryOp::Or
    ) {
        return false;
    }
    source_map.operator_starts_or_ends_line(expression.op_span)
        || source_map.contains_newline_between(
            expression.left.span().end.offset(),
            expression.op_span.start.offset(),
        )
        || source_map.contains_newline_between(
            expression.op_span.end.offset(),
            expression.right.span().start.offset(),
        )
}

pub(super) fn conditional_expr_contains_command_substitution(expression: &ConditionalExpr) -> bool {
    match expression {
        ConditionalExpr::Binary(expr) => {
            conditional_expr_contains_command_substitution(&expr.left)
                || conditional_expr_contains_command_substitution(&expr.right)
        }
        ConditionalExpr::Unary(expr) => conditional_expr_contains_command_substitution(&expr.expr),
        ConditionalExpr::Parenthesized(expr) => {
            conditional_expr_contains_command_substitution(&expr.expr)
        }
        ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
            word_contains_command_substitution(word)
        }
        ConditionalExpr::Pattern(pattern) => pattern_contains_command_substitution(pattern),
        ConditionalExpr::VarRef(_) => false,
    }
}

fn pattern_contains_command_substitution(pattern: &Pattern) -> bool {
    pattern.parts.iter().any(|part| match &part.kind {
        PatternPart::Group { patterns, .. } => {
            patterns.iter().any(pattern_contains_command_substitution)
        }
        PatternPart::Word(word) => word_contains_command_substitution(word),
        PatternPart::Literal(_)
        | PatternPart::AnyString
        | PatternPart::AnyChar
        | PatternPart::CharClass(_) => false,
    })
}

#[derive(Clone, Copy, Eq, PartialEq)]
enum WordSubstitutionKind {
    Command,
    Process,
}

fn word_contains_substitution(word: &Word, kind: WordSubstitutionKind) -> bool {
    word.parts
        .iter()
        .any(|part| word_part_contains_substitution(&part.kind, kind))
}

pub(super) fn word_contains_command_substitution(word: &Word) -> bool {
    word_contains_substitution(word, WordSubstitutionKind::Command)
}

pub(super) fn word_contains_process_substitution(word: &Word) -> bool {
    word_contains_substitution(word, WordSubstitutionKind::Process)
}

pub(super) fn word_source_has_shell_substitution(word: &Word, source: &str) -> bool {
    let raw = word.span.slice(source);
    rendered_text_has_shell_substitution(raw)
}

pub(super) fn rendered_text_has_shell_substitution(text: &str) -> bool {
    text.contains("$(") || text.contains('`') || text.contains("<(") || text.contains(">(")
}

pub(super) fn rendered_text_starts_with_block_command_substitution(text: &str) -> bool {
    text.lines()
        .next()
        .is_some_and(|line| line.trim_end_matches([' ', '\t', '\r']).ends_with("$("))
}

pub(super) fn rendered_text_starts_like_assignment_with_substitution(text: &str) -> bool {
    let first_line = text.lines().next().unwrap_or(text);
    let substitution_start = ["$(", "`", "<(", ">("]
        .iter()
        .filter_map(|marker| first_line.find(marker))
        .min()
        .unwrap_or(first_line.len());
    first_line[..substitution_start].contains('=')
}

pub(super) fn rendered_text_has_leading_list_operator_line(text: &str) -> bool {
    text.lines().skip(1).any(|line| {
        let trimmed = line.trim_start_matches([' ', '\t', '\r']);
        (trimmed.starts_with('|') && !trimmed.starts_with("|)")) || trimmed.starts_with("&&")
    })
}

fn word_part_contains_substitution(part: &WordPart, kind: WordSubstitutionKind) -> bool {
    match part {
        WordPart::CommandSubstitution { .. } => kind == WordSubstitutionKind::Command,
        WordPart::ProcessSubstitution { .. } => kind == WordSubstitutionKind::Process,
        WordPart::DoubleQuoted { parts, .. } => parts
            .iter()
            .any(|part| word_part_contains_substitution(&part.kind, kind)),
        WordPart::ArithmeticExpansion {
            expression_word_ast,
            ..
        } => word_contains_substitution(expression_word_ast, kind),
        WordPart::ParameterExpansion {
            operand_word_ast, ..
        } => operand_word_ast
            .as_deref()
            .is_some_and(|word| word_contains_substitution(word, kind)),
        WordPart::IndirectExpansion {
            operand_word_ast, ..
        } => operand_word_ast
            .as_deref()
            .is_some_and(|word| word_contains_substitution(word, kind)),
        WordPart::Substring {
            offset_word_ast,
            length_word_ast,
            ..
        }
        | WordPart::ArraySlice {
            offset_word_ast,
            length_word_ast,
            ..
        } => {
            word_contains_substitution(offset_word_ast, kind)
                || length_word_ast
                    .as_deref()
                    .is_some_and(|word| word_contains_substitution(word, kind))
        }
        _ => false,
    }
}

impl<'source, 'facts, S> ShellRenderer<'source, 'facts, S>
where
    S: StreamSink,
{
    pub(super) fn write_rendered_shell_text(&mut self, text: &str) {
        if text.contains('\n') {
            if self.line_start() {
                self.write_indent();
            }
            self.write_verbatim(text);
        } else {
            self.write_text(text);
        }
    }

    pub(super) fn write_text_preserving_current_line_indent(&mut self, text: &str) {
        if text.is_empty() {
            return;
        }

        let base_indent_column = if self.line_start() {
            self.indent_column_for_level(self.indent_level())
        } else {
            self.line_indent_column()
        };
        let mut active_heredoc: Option<RenderedHeredocTail> = None;
        let mut remaining = text;
        while !remaining.is_empty() {
            let (line, next, had_newline) = split_first_line(remaining);

            if let Some(heredoc) = active_heredoc.as_ref() {
                if heredoc.strip_tabs {
                    if self.line_start() && !line.is_empty() {
                        self.write_indent_to_column(base_indent_column);
                    }
                    self.push_output_str(line);
                    self.writer.set_line_start(false);
                } else {
                    self.write_verbatim(heredoc.body_line(line));
                }
                if heredoc.closes(line) {
                    active_heredoc = None;
                }
            } else {
                if self.line_start() && !line.is_empty() {
                    self.write_indent_to_column(base_indent_column);
                }
                self.push_output_str(line);
                self.writer.set_line_start(false);
                active_heredoc = rendered_heredoc_tail_start(line);
            }

            if had_newline {
                self.push_output_str(self.line_ending());
                self.writer.set_line_start(true);
            }
            remaining = next;
        }
    }

    pub(super) fn write_command_substitution_assignment_text(&mut self, text: &str) {
        if text.is_empty() {
            return;
        }

        let base_indent_column = if self.line_start() {
            self.indent_column_for_level(self.indent_level())
        } else {
            self.line_indent_column()
        };
        let starts_with_block_command_substitution = text
            .lines()
            .next()
            .is_some_and(|line| line.trim_end_matches([' ', '\t', '\r']).ends_with("$("));
        let strip_context_indent = !starts_with_block_command_substitution;
        let indent_unit = self.options.indent_unit_columns();
        let inline_pipeline_indent_column = base_indent_column + indent_unit;
        let mut next_pipeline_indent_column = None;
        let mut active_shell_pipeline_indent_column: Option<usize> = None;
        let mut active_shell_line_was_pipeline_stage = false;
        let mut next_block_line_is_pipeline_stage = false;
        let mut next_block_line_aligns_with_command_continuation = false;
        let mut command_continuation_active = false;
        let mut pipeline_quote_state = RawLineQuoteState::default();
        let mut remaining = text;
        while !remaining.is_empty() {
            let line_started_as_command_continuation = command_continuation_active;
            let pipeline_indent_column = next_pipeline_indent_column;
            let closes_block_command_substitution = starts_with_block_command_substitution
                && command_substitution_assignment_line_closes_block(remaining);
            let close_line_has_context_indent = closes_block_command_substitution
                && remaining.lines().next().is_some_and(|line| {
                    rendered_line_indent_column(line, self.options()) >= base_indent_column
                });
            let pipeline_stage_indent = self.line_start()
                && !remaining.starts_with('\n')
                && pipeline_indent_column.is_some()
                && !closes_block_command_substitution
                && !remaining
                    .trim_start_matches([' ', '\t', '\r'])
                    .starts_with('\n');
            let add_context_indent = self.line_start()
                && !remaining.starts_with('\n')
                && !pipeline_stage_indent
                && !close_line_has_context_indent
                && command_substitution_assignment_line_needs_context_indent(
                    remaining,
                    self.options(),
                );
            if pipeline_stage_indent {
                self.write_indent_to_column(pipeline_indent_column.unwrap_or_default());
            }
            if add_context_indent {
                self.write_indent_to_column(base_indent_column);
            }

            let (line, next, had_newline) = split_first_line_including_newline(remaining);
            let line = if pipeline_stage_indent {
                line.trim_start_matches([' ', '\t'])
            } else if add_context_indent && strip_context_indent {
                strip_assignment_context_indent(line, base_indent_column, self.options())
            } else {
                line
            };
            if had_newline {
                let adjusted_block_pipeline_stage;
                let line = if starts_with_block_command_substitution
                    && next_block_line_is_pipeline_stage
                    && next_block_line_aligns_with_command_continuation
                    && !pipeline_stage_indent
                    && let Some(shell_indent_column) = active_shell_pipeline_indent_column
                {
                    let target_column = shell_indent_column.saturating_sub(base_indent_column);
                    let line_indent_column = rendered_line_indent_column(line, self.options());
                    if line_indent_column > target_column {
                        adjusted_block_pipeline_stage = Some(rendered_line_with_indent_column(
                            line,
                            target_column,
                            self.options(),
                        ));
                        adjusted_block_pipeline_stage.as_deref().unwrap_or(line)
                    } else {
                        line
                    }
                } else {
                    line
                };
                let emitted_indent_column = emitted_line_indent_column(
                    line,
                    pipeline_indent_column,
                    add_context_indent,
                    base_indent_column,
                    self.options(),
                );
                if let Some(shell_indent_column) = command_substitution_shell_text_indent_column(
                    line,
                    pipeline_quote_state.in_quote(),
                    emitted_indent_column,
                    base_indent_column,
                    indent_unit,
                ) {
                    active_shell_pipeline_indent_column = Some(shell_indent_column);
                    active_shell_line_was_pipeline_stage =
                        pipeline_stage_indent || next_block_line_is_pipeline_stage;
                    next_block_line_is_pipeline_stage = false;
                    next_block_line_aligns_with_command_continuation = false;
                }
                self.push_output_str(line);
                let line_continues_command = !pipeline_quote_state.in_quote()
                    && line_without_continuation_backslash(line.trim_end_matches('\n')).is_some();
                let continuation = command_substitution_pipeline_stage_continuation(
                    line,
                    pipeline_stage_indent,
                    &mut pipeline_quote_state,
                );
                next_pipeline_indent_column = next_command_substitution_pipeline_indent_column(
                    continuation,
                    starts_with_block_command_substitution,
                    inline_pipeline_indent_column,
                    active_shell_pipeline_indent_column,
                    active_shell_line_was_pipeline_stage,
                    indent_unit,
                    pipeline_indent_column,
                );
                if matches!(
                    continuation,
                    CommandSubstitutionPipelineContinuation::StructuralPipe {
                        line_started_in_quote: false
                    }
                ) && starts_with_block_command_substitution
                {
                    next_block_line_is_pipeline_stage = true;
                    next_block_line_aligns_with_command_continuation =
                        line_started_as_command_continuation;
                }
                command_continuation_active = line_continues_command;
                self.writer.set_line_start(true);
                remaining = next;
            } else {
                self.push_output_str(line);
                self.writer.set_line_start(false);
                break;
            }
        }
    }

    pub(super) fn write_shell_text_with_heredoc_tails(
        &mut self,
        text: &str,
        assignment_context: bool,
    ) {
        if assignment_context && !rendered_text_starts_with_block_command_substitution(text) {
            let base_indent_column = if self.line_start() {
                self.indent_column_for_level(self.indent_level())
            } else if self.line_indent_column() > 0 {
                self.line_indent_column()
            } else {
                self.column()
            };
            if self.line_start() {
                self.write_indent_to_column(base_indent_column);
            }
            self.write_shell_text_preserving_heredoc_tails(text, HeredocTailTextMode::Assignment);
            return;
        }
        self.write_shell_text_preserving_heredoc_tails(text, HeredocTailTextMode::Rendered);
    }

    pub(super) fn write_shell_text_preserving_heredoc_tails(
        &mut self,
        text: &str,
        mode: HeredocTailTextMode,
    ) {
        let mut active_heredoc: Option<RenderedHeredocTail> = None;
        let mut rest = text;

        while !rest.is_empty() {
            let (line, next, had_newline) = split_first_line(rest);

            if let Some(heredoc) = active_heredoc.as_ref() {
                self.write_verbatim(heredoc.body_line(line));
                if heredoc.closes(line) {
                    active_heredoc = None;
                }
            } else {
                let heredoc = rendered_heredoc_tail_start(line);
                let normalized = heredoc
                    .is_some()
                    .then(|| normalize_rendered_heredoc_start_spacing(line))
                    .flatten();
                let line = if self.options().space_redirects() {
                    line
                } else {
                    normalized.as_deref().unwrap_or(line)
                };
                match mode {
                    HeredocTailTextMode::Rendered => self.write_text(line),
                    HeredocTailTextMode::Assignment => self.write_verbatim(line),
                }
                active_heredoc = heredoc;
            }

            if had_newline {
                self.push_output_str(self.line_ending());
                self.writer.set_line_start(true);
            }
            rest = next;
        }
    }

    fn write_rendered_fragment(&mut self, fragment: RenderedFragment<'_>) {
        match fragment {
            RenderedFragment::Plain(text) => self.write_text(text.as_ref()),
            RenderedFragment::RenderedShellText(text) => {
                self.write_rendered_shell_text(text.as_ref());
            }
            RenderedFragment::PreserveCurrentIndent(text) => {
                self.write_text_preserving_current_line_indent(text.as_ref());
            }
            RenderedFragment::AssignmentCommandSubstitution(text) => {
                self.write_command_substitution_assignment_text(text.as_ref());
            }
            RenderedFragment::HeredocTail {
                text,
                assignment_context,
            } => {
                self.write_shell_text_with_heredoc_tails(text.as_ref(), assignment_context);
            }
        }
    }

    fn classify_rendered_word_fragment<'text>(
        &self,
        word: &Word,
        rendered: &'text str,
    ) -> RenderedFragment<'text> {
        let has_newline = rendered.contains('\n');
        let mut has_command_substitution = None;
        let mut has_rendered_shell_substitution = None;
        let mut word_has_command_substitution = || {
            *has_command_substitution
                .get_or_insert_with(|| word_contains_command_substitution(word))
        };
        let mut rendered_has_shell_substitution = || {
            *has_rendered_shell_substitution
                .get_or_insert_with(|| rendered_text_has_shell_substitution(rendered))
        };

        if rendered_shell_text_has_heredoc_tail(rendered)
            && (word_contains_command_heredoc(word, self.facts())
                || word_source_has_shell_substitution(word, self.source())
                || rendered_has_shell_substitution())
        {
            return RenderedFragment::HeredocTail {
                text: Cow::Borrowed(rendered),
                assignment_context: true,
            };
        }

        if has_newline
            && (word_is_quoted_formattable_command_substitution_only_with_facts(word, self.facts())
                || word_contains_process_substitution(word))
        {
            return RenderedFragment::PreserveCurrentIndent(Cow::Borrowed(rendered));
        }

        if self.facts().word_has_multiline_literal_source(word) {
            if has_newline
                && (word_has_command_substitution() || rendered_has_shell_substitution())
                && let Some(normalized) =
                    normalize_rendered_leading_list_operator_continuations(rendered)
            {
                return RenderedFragment::AssignmentCommandSubstitution(Cow::Owned(normalized));
            }
            return RenderedFragment::RenderedShellText(Cow::Borrowed(rendered));
        }

        if has_newline && (word_has_command_substitution() || rendered_has_shell_substitution()) {
            if rendered_text_has_leading_list_operator_line(rendered) {
                RenderedFragment::AssignmentCommandSubstitution(Cow::Borrowed(rendered))
            } else {
                RenderedFragment::PreserveCurrentIndent(Cow::Borrowed(rendered))
            }
        } else {
            RenderedFragment::Plain(Cow::Borrowed(rendered))
        }
    }

    fn classify_assignment_source_fragment(
        &self,
        assignment: &Assignment,
    ) -> Option<RenderedFragment<'source>> {
        if assignment_has_quoted_backslash_continuation_literal(assignment, self.source()) {
            return Some(RenderedFragment::RenderedShellText(Cow::Borrowed(
                assignment.span.slice(self.source()),
            )));
        }

        normalize_scalar_assignment_unquoted_continuations(assignment, self.source(), self.facts())
            .map(|normalized| RenderedFragment::Plain(Cow::Owned(normalized)))
    }

    fn classify_rendered_assignment_fragment<'text>(
        &self,
        assignment: &Assignment,
        rendered: &'text str,
    ) -> RenderedFragment<'text> {
        let has_newline = rendered.contains('\n');
        let mut has_command_substitution = None;
        let mut has_rendered_shell_substitution = None;
        let mut assignment_has_command_substitution = || {
            *has_command_substitution.get_or_insert_with(|| {
                assignment_source_has_command_substitution(assignment, self.source())
            })
        };
        let mut rendered_has_shell_substitution = || {
            *has_rendered_shell_substitution
                .get_or_insert_with(|| rendered_text_has_shell_substitution(rendered))
        };

        if rendered_shell_text_has_heredoc_tail(rendered)
            && (assignment_contains_command_heredoc(assignment, self.facts())
                || assignment_has_command_substitution()
                || rendered_has_shell_substitution())
        {
            return RenderedFragment::HeredocTail {
                text: Cow::Borrowed(rendered),
                assignment_context: true,
            };
        }

        if has_newline
            && assignment_value_is_quoted_formattable_command_substitution_only(
                assignment,
                self.facts(),
            )
        {
            return RenderedFragment::PreserveCurrentIndent(Cow::Borrowed(rendered));
        }

        if has_newline && assignment_has_command_substitution() {
            if compound_assignment_is_single_case_command_substitution(assignment) {
                return RenderedFragment::PreserveCurrentIndent(Cow::Borrowed(rendered));
            }

            if self
                .facts()
                .assignment_has_multiline_literal_source(assignment, self.source())
            {
                if assignment_value_is_quoted_command_substitution_only(assignment) {
                    return RenderedFragment::AssignmentCommandSubstitution(Cow::Borrowed(
                        rendered,
                    ));
                }

                if assignment_source_has_leading_pipe_continuation(assignment, self.source()) {
                    return RenderedFragment::PreserveCurrentIndent(Cow::Borrowed(rendered));
                }

                let continuation_indent = self
                    .options
                    .indent_prefix(self.indent_level().saturating_add(1));
                let normalized = normalize_literal_assignment_command_substitution_pipelines(
                    rendered,
                    &continuation_indent,
                );
                return RenderedFragment::RenderedShellText(Cow::Owned(normalized));
            }

            return RenderedFragment::PreserveCurrentIndent(Cow::Borrowed(rendered));
        }

        if self
            .facts()
            .assignment_has_multiline_literal_source(assignment, self.source())
        {
            RenderedFragment::RenderedShellText(Cow::Borrowed(rendered))
        } else {
            RenderedFragment::Plain(Cow::Borrowed(rendered))
        }
    }

    fn classify_rendered_name_fragment<'text>(
        &self,
        rendered_name: &'text str,
    ) -> RenderedFragment<'text> {
        if rendered_shell_text_has_heredoc_tail(rendered_name)
            && rendered_text_has_shell_substitution(rendered_name)
        {
            RenderedFragment::HeredocTail {
                text: Cow::Borrowed(rendered_name),
                assignment_context: rendered_text_starts_like_assignment_with_substitution(
                    rendered_name,
                ),
            }
        } else {
            RenderedFragment::Plain(Cow::Borrowed(rendered_name))
        }
    }

    pub(super) fn write_word(&mut self, word: &Word) {
        let mut scratch = self.take_scratch_buffer();
        self.render_word_to_buffer(word, &mut scratch);
        let fragment = self.classify_rendered_word_fragment(word, &scratch);
        self.write_rendered_fragment(fragment);
        self.restore_scratch_buffer(scratch);
    }

    pub(super) fn write_pattern(&mut self, pattern: &Pattern) {
        let mut scratch = self.take_scratch_buffer();
        render_pattern_syntax_to_buf(pattern, self.render_context(), &mut scratch);
        self.write_text(&scratch);
        self.restore_scratch_buffer(scratch);
    }

    pub(super) fn write_case_pattern(&mut self, item: &CaseItem, pattern: &Pattern) {
        let mut scratch = self.take_scratch_buffer();
        render_pattern_syntax_to_buf(pattern, self.render_context(), &mut scratch);
        if case_item_pattern_close_paren_on_own_line(item, self.source(), self.source_map()) {
            trim_trailing_pattern_line_continuation(&mut scratch);
        }
        self.write_text(&scratch);
        self.restore_scratch_buffer(scratch);
    }

    pub(super) fn write_var_ref(&mut self, reference: &VarRef) {
        self.write_rendered(|scratch, source, _| {
            render_var_ref_to_buf(reference, source, scratch);
        });
    }

    pub(super) fn write_assignment(&mut self, assignment: &Assignment) {
        if let Some(fragment) = self.classify_assignment_source_fragment(assignment) {
            self.write_rendered_fragment(fragment);
            return;
        }

        let mut scratch = self.take_scratch_buffer();
        render_assignment_to_buf(assignment, self.render_context(), &mut scratch);
        let fragment = self.classify_rendered_assignment_fragment(assignment, &scratch);
        self.write_rendered_fragment(fragment);
        self.restore_scratch_buffer(scratch);
    }

    pub(super) fn write_assignment_head(&mut self, assignment: &Assignment) {
        self.write_rendered(|scratch, source, _| {
            render_assignment_head_to_buf(assignment, source, scratch);
        });
    }

    pub(super) fn write_rendered_name_text(&mut self, rendered_name: &str) {
        let fragment = self.classify_rendered_name_fragment(rendered_name);
        self.write_rendered_fragment(fragment);
    }

    pub(super) fn format_standalone_multiline_compound_assignment(
        &mut self,
        assignment: &shuck_ast::Assignment,
    ) -> Result<()> {
        let source = self.source();
        if compound_assignment_is_single_case_command_substitution(assignment) {
            self.write_assignment(assignment);
            return Ok(());
        }

        if self.format_escaped_multiline_double_quoted_compound_assignment(assignment) {
            return Ok(());
        }

        if self.compound_assignment_should_preserve_multiline_literal_layout(assignment) {
            self.write_multiline_compound_literal_assignment(assignment);
            return Ok(());
        }

        let Some(layout) = multiline_compound_assignment_layout(assignment, source) else {
            self.write_assignment(assignment);
            return Ok(());
        };

        self.write_assignment_head(assignment);
        self.write_text("(");
        self.write_standalone_multiline_compound_assignment_layout(&layout);
        Ok(())
    }

    pub(super) fn compound_assignment_source_has_line_continuations(raw: &str) -> bool {
        raw.contains("\\\n") || raw.contains("\\\r\n")
    }

    pub(super) fn compound_assignment_source_has_escaped_multiline_double_quoted_item(
        raw: &str,
    ) -> bool {
        if !Self::compound_assignment_source_has_line_continuations(raw) {
            return false;
        }

        let Some(open) = raw.find('(') else {
            return false;
        };
        let Some(close) = raw.rfind(')') else {
            return false;
        };
        if close <= open {
            return false;
        }

        raw.get(open + 1..close)
            .is_some_and(|body| body.contains("\"\\\n") || body.contains("\"\\\r\n"))
    }

    pub(super) fn compound_assignment_should_preserve_multiline_literal_layout(
        &self,
        assignment: &Assignment,
    ) -> bool {
        let source = self.source();
        if !self
            .facts()
            .assignment_has_multiline_literal_source(assignment, source)
        {
            return false;
        }

        let raw = assignment.span.slice(source);
        !Self::compound_assignment_source_has_line_continuations(raw)
    }

    pub(super) fn format_escaped_multiline_double_quoted_compound_assignment(
        &mut self,
        assignment: &Assignment,
    ) -> bool {
        if !Self::compound_assignment_source_has_escaped_multiline_double_quoted_item(
            assignment.span.slice(self.source()),
        ) {
            return false;
        }

        let AssignmentValue::Compound(array) = &assignment.value else {
            return false;
        };

        self.write_assignment_head(assignment);
        self.write_text("(");
        for (index, element) in array.elements.iter().enumerate() {
            if index > 0 {
                self.newline();
                self.write_indent_units(1);
            }
            self.write_array_element(element, true);
        }
        self.write_text(")");
        true
    }

    pub(super) fn write_word_with_escaped_multiline_substitution_indent(&mut self, word: &Word) {
        let mut scratch = self.take_scratch_buffer();
        render_escaped_multiline_word_syntax_to_buf(word, self.render_context(), &mut scratch);

        let normalized =
            normalize_escaped_multiline_word_command_substitution_indent(&scratch, self.options());
        let rendered = normalized.as_deref().unwrap_or(&scratch);
        if rendered.contains('\n')
            && rendered_text_has_shell_substitution(rendered)
            && let Some(normalized) =
                normalize_rendered_leading_list_operator_continuations(rendered)
        {
            self.write_command_substitution_assignment_text(&normalized);
        } else if rendered.contains('\n') {
            self.write_rendered_shell_text(rendered);
        } else {
            self.write_text(rendered);
        }
        self.restore_scratch_buffer(scratch);
    }

    pub(super) fn write_multiline_compound_literal_assignment(&mut self, assignment: &Assignment) {
        let raw = assignment.span.slice(self.source());
        let Some((head, tail)) = raw.split_once('\n') else {
            self.write_text(raw);
            return;
        };

        self.write_text(head);
        let mut quote = multiline_literal_quote_state_after_line(head, None);
        for line in tail.lines() {
            self.newline();
            if quote.is_some() {
                self.write_verbatim(line.trim_end_matches('\r'));
                quote = multiline_literal_quote_state_after_line(line, quote);
                continue;
            }

            let trimmed = line.trim_start_matches([' ', '\t']).trim_end_matches('\r');
            if trimmed.starts_with(')') {
                self.write_text(trimmed);
            } else {
                self.write_indent_units(1);
                self.write_text(trimmed);
            }
            quote = multiline_literal_quote_state_after_line(trimmed, quote);
        }
    }
}
