use super::*;

#[derive(Debug, Clone, Copy)]
pub struct ListOperatorFact {
    pub(crate) op: BinaryOp,
    pub(crate) span: Span,
}

impl ListOperatorFact {
    pub fn op(&self) -> BinaryOp {
        self.op
    }

    pub fn span(&self) -> Span {
        self.span
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListSegmentKind {
    Condition,
    AssignmentOnly,
    Other,
}

#[derive(Debug, Clone)]
pub struct ListSegmentFact<'a> {
    command_id: CommandId,
    span: Span,
    kind: ListSegmentKind,
    assignment_target: Option<&'a str>,
    assignment_span: Option<Span>,
    assignment_is_declaration: bool,
}

impl<'a> ListSegmentFact<'a> {
    pub fn command_id(&self) -> CommandId {
        self.command_id
    }

    pub fn span(&self) -> Span {
        self.span
    }

    pub fn kind(&self) -> ListSegmentKind {
        self.kind
    }

    pub fn assignment_target(&self) -> Option<&'a str> {
        self.assignment_target
    }

    pub fn assignment_span(&self) -> Option<Span> {
        self.assignment_span
    }

    pub fn assignment_is_declaration(&self) -> bool {
        self.assignment_is_declaration
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MixedShortCircuitKind {
    TestChain,
    AssignmentTernary,
    Fallthrough,
}

#[derive(Debug, Clone)]
pub struct ListFact<'a> {
    command: &'a BinaryCommand,
    operators: Box<[ListOperatorFact]>,
    segments: Box<[ListSegmentFact<'a>]>,
    mixed_short_circuit_span: Option<Span>,
    mixed_short_circuit_kind: Option<MixedShortCircuitKind>,
}

impl<'a> ListFact<'a> {
    pub fn span(&self) -> Span {
        self.command.span
    }

    pub fn operators(&self) -> &[ListOperatorFact] {
        &self.operators
    }

    pub fn segments(&self) -> &[ListSegmentFact<'a>] {
        &self.segments
    }

    pub fn mixed_short_circuit_span(&self) -> Option<Span> {
        self.mixed_short_circuit_span
    }

    pub fn mixed_short_circuit_kind(&self) -> Option<MixedShortCircuitKind> {
        self.mixed_short_circuit_kind
    }
}

#[cfg_attr(shuck_profiling, inline(never))]
pub(crate) fn build_list_facts<'a>(
    commands: &[CommandFact<'a>],
    command_fact_indices_by_id: &[Option<usize>],
    command_ids_by_span: &CommandLookupIndex,
    command_child_index: &CommandChildIndex,
    source: &str,
) -> Vec<ListFact<'a>> {
    let command_relationships = CommandRelationshipContext::new(
        commands,
        command_fact_indices_by_id,
        command_ids_by_span,
        command_child_index,
    );
    let mut nested_list_commands = FxHashSet::default();

    for fact in commands {
        let Command::Binary(command) = fact.command() else {
            continue;
        };
        if BinaryCommandChain::logical_list(command).is_none() {
            continue;
        }

        record_nested_list_command(
            &command.left,
            fact.id(),
            command_relationships,
            &mut nested_list_commands,
        );
        record_nested_list_command(
            &command.right,
            fact.id(),
            command_relationships,
            &mut nested_list_commands,
        );
    }

    commands
        .iter()
        .filter_map(|fact| {
            let Command::Binary(command) = fact.command() else {
                return None;
            };
            if BinaryCommandChain::logical_list(command).is_none()
                || nested_list_commands.contains(&fact.id())
            {
                return None;
            }

            let mut operators = Vec::new();
            collect_short_circuit_operators(command, &mut operators);
            let segments =
                build_list_segment_facts(command, command_relationships, fact.id(), source)?;
            let mixed_short_circuit_span = mixed_short_circuit_operator_span(&operators);
            let mixed_short_circuit_kind = mixed_short_circuit_span
                .map(|_| classify_mixed_short_circuit_kind(&segments, &operators));

            Some(ListFact {
                command,
                operators: operators.into_boxed_slice(),
                segments,
                mixed_short_circuit_span,
                mixed_short_circuit_kind,
            })
        })
        .collect()
}

pub(crate) fn record_nested_list_command(
    stmt: &Stmt,
    parent_id: CommandId,
    command_relationships: CommandRelationshipContext<'_, '_>,
    nested_list_commands: &mut FxHashSet<CommandId>,
) {
    let Command::Binary(child) = &stmt.command else {
        return;
    };
    if BinaryCommandChain::logical_list(child).is_some()
        && let Some(child) = command_relationships.child_or_lookup_fact(parent_id, stmt)
    {
        nested_list_commands.insert(child.id());
    }
}

pub(crate) fn build_list_segment_facts<'a>(
    command: &BinaryCommand,
    command_relationships: CommandRelationshipContext<'_, 'a>,
    parent_id: CommandId,
    source: &str,
) -> Option<Box<[ListSegmentFact<'a>]>> {
    let mut segments = Vec::new();
    collect_list_segment_facts(
        command,
        command_relationships,
        parent_id,
        source,
        &mut segments,
    )?;
    Some(segments.into_boxed_slice())
}

pub(crate) fn collect_list_segment_facts<'a>(
    command: &BinaryCommand,
    command_relationships: CommandRelationshipContext<'_, 'a>,
    parent_id: CommandId,
    source: &str,
    segments: &mut Vec<ListSegmentFact<'a>>,
) -> Option<()> {
    let chain = BinaryCommandChain::logical_list(command)?;
    let mut ok = true;
    chain.visit_segments(|stmt| {
        if ok {
            ok = push_list_stmt_segment_fact(
                stmt,
                command_relationships,
                parent_id,
                source,
                segments,
            )
            .is_some();
        }
    });
    ok.then_some(())
}

pub(crate) fn push_list_stmt_segment_fact<'a>(
    stmt: &Stmt,
    command_relationships: CommandRelationshipContext<'_, 'a>,
    parent_id: CommandId,
    source: &str,
    segments: &mut Vec<ListSegmentFact<'a>>,
) -> Option<()> {
    let fact = command_relationships.child_or_lookup_fact(parent_id, stmt)?;
    let id = fact.id();
    let assignment_info = list_segment_assignment_info(fact);
    let assignment_target = assignment_info.as_ref().map(|info| info.target);
    let assignment_is_declaration = assignment_info
        .as_ref()
        .is_some_and(|info| info.is_declaration);

    segments.push(ListSegmentFact {
        command_id: id,
        span: fact.span_in_source(source),
        kind: list_segment_kind(fact),
        assignment_target,
        assignment_span: assignment_info.map(|info| info.span),
        assignment_is_declaration,
    });
    Some(())
}

pub(crate) fn list_segment_kind(fact: &CommandFact<'_>) -> ListSegmentKind {
    if list_segment_is_condition(fact) {
        ListSegmentKind::Condition
    } else if list_segment_assignment_target(fact).is_some() {
        ListSegmentKind::AssignmentOnly
    } else {
        ListSegmentKind::Other
    }
}

pub(crate) fn list_segment_is_condition(fact: &CommandFact<'_>) -> bool {
    fact.simple_test().is_some()
        || fact.conditional().is_some()
        || matches!(fact.effective_or_literal_name(), Some("true" | "false"))
}

pub(crate) fn list_segment_assignment_target<'a>(fact: &CommandFact<'a>) -> Option<&'a str> {
    list_segment_assignment_info(fact).map(|info| info.target)
}

#[derive(Clone, Copy)]
pub(crate) struct ListSegmentAssignmentInfo<'a> {
    target: &'a str,
    span: Span,
    is_declaration: bool,
}

pub(crate) fn list_segment_assignment_info<'a>(
    fact: &CommandFact<'a>,
) -> Option<ListSegmentAssignmentInfo<'a>> {
    match fact.command() {
        Command::Simple(command)
            if command.args.is_empty()
                && !command.assignments.is_empty()
                && fact.literal_name() == Some("") =>
        {
            single_assignment_info(&command.assignments)
        }
        Command::Decl(command) => declaration_assignment_info(command),
        _ => None,
    }
}

pub(crate) fn single_assignment_info<'a>(
    assignments: &'a [Assignment],
) -> Option<ListSegmentAssignmentInfo<'a>> {
    (assignments.len() == 1).then(|| ListSegmentAssignmentInfo {
        target: assignments[0].target.name.as_str(),
        span: assignments[0].span,
        is_declaration: false,
    })
}

pub(crate) fn declaration_assignment_info<'a>(
    command: &'a DeclClause,
) -> Option<ListSegmentAssignmentInfo<'a>> {
    if !command.assignments.is_empty() {
        return None;
    }

    let mut assignment = None;

    for operand in &command.operands {
        match operand {
            DeclOperand::Flag(_) => {}
            DeclOperand::Assignment(candidate) => {
                if assignment.replace(candidate).is_some() {
                    return None;
                }
            }
            DeclOperand::Name(_) | DeclOperand::Dynamic(_) => return None,
        }
    }

    assignment.map(|assignment| ListSegmentAssignmentInfo {
        target: assignment.target.name.as_str(),
        span: assignment.span,
        is_declaration: true,
    })
}

pub(crate) fn classify_mixed_short_circuit_kind(
    segments: &[ListSegmentFact<'_>],
    operators: &[ListOperatorFact],
) -> MixedShortCircuitKind {
    if segments
        .iter()
        .all(|segment| segment.kind() == ListSegmentKind::Condition)
    {
        MixedShortCircuitKind::TestChain
    } else if matches_assignment_ternary(segments, operators) {
        MixedShortCircuitKind::AssignmentTernary
    } else {
        MixedShortCircuitKind::Fallthrough
    }
}

pub(crate) fn matches_assignment_ternary(
    segments: &[ListSegmentFact<'_>],
    operators: &[ListOperatorFact],
) -> bool {
    let [condition, then_branch, else_branch] = segments else {
        return false;
    };
    let [first_operator, second_operator] = operators else {
        return false;
    };

    condition.kind() == ListSegmentKind::Condition
        && first_operator.op() == BinaryOp::And
        && second_operator.op() == BinaryOp::Or
        && then_branch.kind() == ListSegmentKind::AssignmentOnly
        && else_branch.kind() == ListSegmentKind::AssignmentOnly
        && then_branch.assignment_target().is_some()
        && then_branch.assignment_target() == else_branch.assignment_target()
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct NegativeComparison {
    runtime_operand: String,
    literal_operand: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NumericLiteralNormalization {
    None,
    Decimal,
    ShellArithmetic,
}

#[cfg_attr(shuck_profiling, inline(never))]
pub(super) fn build_tautology_chain_operator_spans<'a>(
    commands: &[CommandFact<'a>],
    command_fact_indices_by_id: &[Option<usize>],
    lists: &[ListFact<'a>],
    source: &str,
) -> Vec<Span> {
    let mut spans = Vec::new();

    for list in lists {
        if !list
            .operators()
            .iter()
            .all(|operator| operator.op() == BinaryOp::Or)
        {
            continue;
        }

        let mut prior_comparisons: Vec<NegativeComparison> = Vec::new();
        for (segment_index, segment) in list.segments().iter().enumerate() {
            let command = command_fact(commands, command_fact_indices_by_id, segment.command_id());
            let Some(comparison) = negative_comparison_for_tautology(command, source) else {
                continue;
            };

            if prior_comparisons.iter().any(|prior| {
                prior.runtime_operand == comparison.runtime_operand
                    && prior.literal_operand != comparison.literal_operand
            }) && let Some(operator) = segment_index
                .checked_sub(1)
                .and_then(|index| list.operators().get(index))
            {
                spans.push(operator.span());
            }

            prior_comparisons.push(comparison);
        }
    }

    sort_and_dedup_spans(&mut spans);
    spans
}

fn negative_comparison_for_tautology(
    command: &CommandFact<'_>,
    source: &str,
) -> Option<NegativeComparison> {
    command
        .simple_test()
        .and_then(|simple_test| simple_test_negative_comparison(simple_test, source))
        .or_else(|| {
            command
                .conditional()
                .and_then(|conditional| conditional_negative_comparison(conditional, source))
        })
}

fn simple_test_negative_comparison(
    simple_test: &SimpleTestFact<'_>,
    source: &str,
) -> Option<NegativeComparison> {
    if simple_test.syntax() != SimpleTestSyntax::Bracket
        || simple_test.effective_operands().len() != 3
    {
        return None;
    }

    let operator = simple_test_effective_operand_text(simple_test, 1, source)?;
    if !matches!(operator.as_str(), "!=" | "-ne") {
        return None;
    }
    let numeric_normalization = if operator == "-ne" {
        NumericLiteralNormalization::Decimal
    } else {
        NumericLiteralNormalization::None
    };

    negative_comparison_from_words(
        simple_test.effective_operands()[0],
        simple_test.effective_operand_class(0)?,
        simple_test.effective_operands()[2],
        simple_test.effective_operand_class(2)?,
        source,
        false,
        numeric_normalization,
    )
}

fn conditional_negative_comparison(
    conditional: &ConditionalFact<'_>,
    source: &str,
) -> Option<NegativeComparison> {
    let ConditionalNodeFact::Binary(binary) = conditional.root() else {
        return None;
    };

    let (skip_pattern_literals, numeric_normalization) = match binary.op() {
        ConditionalBinaryOp::PatternNe => (true, NumericLiteralNormalization::None),
        ConditionalBinaryOp::ArithmeticNe => (false, NumericLiteralNormalization::ShellArithmetic),
        _ => return None,
    };

    negative_comparison_from_operands(
        binary.left(),
        binary.right(),
        source,
        skip_pattern_literals,
        numeric_normalization,
    )
}

fn negative_comparison_from_operands(
    left: ConditionalOperandFact<'_>,
    right: ConditionalOperandFact<'_>,
    source: &str,
    skip_pattern_literals: bool,
    numeric_normalization: NumericLiteralNormalization,
) -> Option<NegativeComparison> {
    match (left.class(), right.class()) {
        (TestOperandClass::FixedLiteral, TestOperandClass::RuntimeSensitive) => {
            let literal = conditional_operand_literal_text(left, source, skip_pattern_literals)?;
            Some(NegativeComparison {
                runtime_operand: conditional_operand_source_text(right, source).to_owned(),
                literal_operand: normalize_tautology_literal(literal, numeric_normalization)?,
            })
        }
        (TestOperandClass::RuntimeSensitive, TestOperandClass::FixedLiteral) => {
            let literal = conditional_operand_literal_text(right, source, skip_pattern_literals)?;
            Some(NegativeComparison {
                runtime_operand: conditional_operand_source_text(left, source).to_owned(),
                literal_operand: normalize_tautology_literal(literal, numeric_normalization)?,
            })
        }
        _ => None,
    }
}

fn negative_comparison_from_words(
    left: &Word,
    left_class: TestOperandClass,
    right: &Word,
    right_class: TestOperandClass,
    source: &str,
    skip_pattern_literals: bool,
    numeric_normalization: NumericLiteralNormalization,
) -> Option<NegativeComparison> {
    match (left_class, right_class) {
        (TestOperandClass::FixedLiteral, TestOperandClass::RuntimeSensitive) => {
            let literal = word_literal_text(left, source, skip_pattern_literals)?;
            Some(NegativeComparison {
                runtime_operand: right.span.slice(source).to_owned(),
                literal_operand: normalize_tautology_literal(literal, numeric_normalization)?,
            })
        }
        (TestOperandClass::RuntimeSensitive, TestOperandClass::FixedLiteral) => {
            let literal = word_literal_text(right, source, skip_pattern_literals)?;
            Some(NegativeComparison {
                runtime_operand: left.span.slice(source).to_owned(),
                literal_operand: normalize_tautology_literal(literal, numeric_normalization)?,
            })
        }
        _ => None,
    }
}

fn conditional_operand_literal_text(
    operand: ConditionalOperandFact<'_>,
    source: &str,
    skip_pattern_literals: bool,
) -> Option<String> {
    let text = if let Some(word) = operand.word() {
        word_literal_text(word, source, skip_pattern_literals)?
    } else {
        let text = operand.expression().span().slice(source).to_owned();
        if skip_pattern_literals && looks_like_conditional_pattern_literal(&text) {
            return None;
        }
        text
    };

    Some(text)
}

fn conditional_operand_source_text<'a>(
    operand: ConditionalOperandFact<'_>,
    source: &'a str,
) -> &'a str {
    operand.expression().span().slice(source)
}

fn word_literal_text(word: &Word, source: &str, skip_pattern_literals: bool) -> Option<String> {
    let text = static_word_text(word, source)?;
    if skip_pattern_literals && word_has_unquoted_conditional_pattern_literal(word, source) {
        return None;
    }

    Some(text.into_owned())
}

fn normalize_tautology_literal(
    literal: String,
    numeric_normalization: NumericLiteralNormalization,
) -> Option<String> {
    match numeric_normalization {
        NumericLiteralNormalization::None => Some(literal),
        NumericLiteralNormalization::Decimal => normalize_decimal_integer_literal(&literal),
        NumericLiteralNormalization::ShellArithmetic => {
            normalize_shell_arithmetic_integer_literal(&literal)
        }
    }
}

fn normalize_decimal_integer_literal(text: &str) -> Option<String> {
    let (negative, unsigned) = if let Some(digits) = text.strip_prefix('-') {
        (true, digits)
    } else if let Some(digits) = text.strip_prefix('+') {
        (false, digits)
    } else {
        (false, text)
    };

    if unsigned.is_empty() || !unsigned.bytes().all(|byte| byte.is_ascii_digit()) {
        return None;
    }

    let value = parse_unsigned_digits(unsigned, 10)?;
    if negative {
        value.checked_neg().map(|value| value.to_string())
    } else {
        Some(value.to_string())
    }
}

fn normalize_shell_arithmetic_integer_literal(text: &str) -> Option<String> {
    parse_shell_arithmetic_integer_value(text).map(|value| value.to_string())
}

fn parse_shell_arithmetic_integer_value(text: &str) -> Option<i128> {
    let (negative, unsigned) = if let Some(digits) = text.strip_prefix('-') {
        (true, digits)
    } else if let Some(digits) = text.strip_prefix('+') {
        (false, digits)
    } else {
        (false, text)
    };
    if unsigned.is_empty() {
        return None;
    }

    let value = if let Some((base_text, digits)) = unsigned.split_once('#') {
        let base = parse_unsigned_digits(base_text, 10)?;
        if !(2..=64).contains(&base) || digits.is_empty() {
            return None;
        }
        parse_arithmetic_digits(digits, base as u32)?
    } else if let Some(digits) = unsigned
        .strip_prefix("0x")
        .or_else(|| unsigned.strip_prefix("0X"))
    {
        if digits.is_empty() {
            return None;
        }
        parse_arithmetic_digits(digits, 16)?
    } else if unsigned.len() > 1 && unsigned.starts_with('0') {
        parse_arithmetic_digits(unsigned, 8)?
    } else {
        parse_unsigned_digits(unsigned, 10)?
    };

    if negative {
        value.checked_neg()
    } else {
        Some(value)
    }
}

fn parse_unsigned_digits(digits: &str, radix: u32) -> Option<i128> {
    if digits.is_empty() {
        return None;
    }
    parse_arithmetic_digits(digits, radix)
}

fn parse_arithmetic_digits(digits: &str, radix: u32) -> Option<i128> {
    let mut value = 0i128;
    let radix_value = i128::from(radix);
    for byte in digits.bytes() {
        let digit = i128::from(arithmetic_digit_value(byte, radix)?);
        if digit >= radix_value {
            return None;
        }
        value = value.checked_mul(radix_value)?.checked_add(digit)?;
    }
    Some(value)
}

fn arithmetic_digit_value(byte: u8, radix: u32) -> Option<u32> {
    match byte {
        b'0'..=b'9' => Some(u32::from(byte - b'0')),
        b'a'..=b'z' => Some(u32::from(byte - b'a') + 10),
        b'A'..=b'Z' if radix <= 36 => Some(u32::from(byte - b'A') + 10),
        b'A'..=b'Z' => Some(u32::from(byte - b'A') + 36),
        b'@' => Some(62),
        b'_' => Some(63),
        _ => None,
    }
}

fn word_has_unquoted_conditional_pattern_literal(word: &Word, source: &str) -> bool {
    !word_spans::word_unquoted_glob_pattern_spans(word, source).is_empty()
        || word_has_unquoted_conditional_extglob_operator(&word.parts, source, false)
}

fn word_has_unquoted_conditional_extglob_operator(
    parts: &[WordPartNode],
    source: &str,
    in_double_quotes: bool,
) -> bool {
    let mut literal_run_start = None::<usize>;
    let mut literal_run_end = None::<usize>;

    let flush_literal_run = |literal_run_start: &mut Option<usize>,
                             literal_run_end: &mut Option<usize>| {
        let Some(start_index) = literal_run_start.take() else {
            return false;
        };
        let Some(end_index) = literal_run_end.take() else {
            return false;
        };
        let start = parts[start_index].span.start;
        let end = parts[end_index].span.end;
        unquoted_text_has_conditional_extglob_operator(
            Span::from_positions(start, end).slice(source),
        )
    };

    for (index, part) in parts.iter().enumerate() {
        match &part.kind {
            WordPart::DoubleQuoted { parts, .. } => {
                if flush_literal_run(&mut literal_run_start, &mut literal_run_end) {
                    return true;
                }
                if word_has_unquoted_conditional_extglob_operator(parts, source, true) {
                    return true;
                }
            }
            WordPart::Literal(_) if !in_double_quotes => {
                literal_run_start.get_or_insert(index);
                literal_run_end = Some(index);
            }
            WordPart::Literal(_)
            | WordPart::CommandSubstitution { .. }
            | WordPart::ProcessSubstitution { .. }
            | WordPart::SingleQuoted { .. }
            | WordPart::Variable(_)
            | WordPart::ArithmeticExpansion { .. }
            | WordPart::Parameter(_)
            | WordPart::ParameterExpansion { .. }
            | WordPart::Length(_)
            | WordPart::ArrayAccess(_)
            | WordPart::ArrayLength(_)
            | WordPart::ArrayIndices(_)
            | WordPart::Substring { .. }
            | WordPart::ArraySlice { .. }
            | WordPart::IndirectExpansion { .. }
            | WordPart::PrefixMatch { .. }
            | WordPart::Transformation { .. }
            | WordPart::ZshQualifiedGlob(_) => {
                if flush_literal_run(&mut literal_run_start, &mut literal_run_end) {
                    return true;
                }
            }
        }
    }

    flush_literal_run(&mut literal_run_start, &mut literal_run_end)
}

fn unquoted_text_has_conditional_extglob_operator(text: &str) -> bool {
    let bytes = text.as_bytes();
    let mut index = 0usize;
    while index + 1 < bytes.len() {
        if matches!(bytes[index], b'@' | b'!' | b'+')
            && bytes[index + 1] == b'('
            && !byte_is_escaped(bytes, index)
        {
            return true;
        }
        index += 1;
    }
    false
}

fn byte_is_escaped(bytes: &[u8], index: usize) -> bool {
    let mut slash_count = 0usize;
    let mut cursor = index;
    while cursor > 0 && bytes[cursor - 1] == b'\\' {
        slash_count += 1;
        cursor -= 1;
    }
    slash_count % 2 == 1
}

fn looks_like_conditional_pattern_literal(text: &str) -> bool {
    let mut chars = text.chars().peekable();
    while let Some(ch) = chars.next() {
        if matches!(ch, '*' | '?' | '[' | ']') {
            return true;
        }

        if matches!(ch, '@' | '!' | '+') && matches!(chars.peek(), Some('(')) {
            return true;
        }
    }

    false
}
