use std::ops::ControlFlow;

use nu_protocol::{
    BlockId, ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span, Type, VarId,
    ast::{
        Argument, Call, Expr, Expression, FindMapResult, FullCellPath, ListItem, Operator,
        PathMember, RecordItem, Traverse,
    },
    engine::Variable,
};

use super::{block::BlockExt, call::CallExt, pipeline::PipelineExt};
use crate::{
    context::LintContext,
    effect::external::{ExternEffect, has_external_side_effect},
};

pub trait ExpressionExt: Traverse {
    /// Extracts the variable name from an expression. Example: `$counter`
    /// returns "counter"
    fn extract_variable_name(&self, context: &LintContext) -> Option<String>;
    /// Checks if expression is an assignment operation. Example: `$x = 5` or
    /// `$x += 1`
    fn is_assignment(&self) -> bool;
    /// Checks if expression is an empty list literal. Example: `[]`
    fn is_empty_list(&self) -> bool;
    /// Extracts block ID from block-like expressions. Example: `{ $in | length
    /// }` or closure
    fn extract_block_id(&self) -> Option<BlockId>;
    /// Returns the source text of this expression's span. Example: `$in | each
    /// { ... }`
    fn span_text<'a>(&self, context: &'a LintContext) -> &'a str;
    /// Extracts the variable being assigned to. Example: `$result = 42` returns
    /// `$result`
    fn extract_assigned_variable(&self) -> Option<VarId>;
    /// Extracts field access from full cell path. Example: `$record.name`
    /// matches "name"
    fn extract_field_access(&self, field_name: &str) -> Option<(VarId, Span)>;

    /// Checks if expression contains any variable references. Example: `$x +
    /// $y` or `[$item]`
    fn contains_variables(&self, context: &LintContext) -> bool;
    /// Checks if external call head uses a variable. Example: `^$cmd arg1 arg2`
    fn is_external_call_with_variable(&self, var_id: VarId) -> bool;
    /// Checks if expression matches a specific variable. Example: `$var` or
    /// `$var.field`
    fn matches_var(&self, var_id: VarId) -> bool;
    /// Extracts `VarId` only from direct variable references (no field access).
    /// Returns `Some(var_id)` for `$x`, `None` for `$x.field`.
    fn extract_direct_var(&self) -> Option<VarId>;
    /// Checks if expression contains a specific variable. Example: `$x + 1`
    /// contains `$x`
    fn contains_variable(&self, var_id: VarId) -> bool;

    /// Checks if expression uses pipeline input variable. Example: `$in` or
    /// `$in | length`
    fn uses_pipeline_input(&self, context: &LintContext) -> bool;
    /// Finds pipeline input-like variables (includes `$in` and closure
    /// parameters) and their spans. Used for type inference. Example:
    /// `$in.field` returns (`var_id`, span of `$in`)
    fn find_pipeline_input(&self, context: &LintContext) -> Option<(VarId, Span)>;
    /// Finds the actual `$in` variable usage and its span. Example: `$in.field`
    /// returns span of `$in`. Does not match closure parameters.
    fn find_dollar_in_usage(&self) -> Option<Span>;
    /// Infers the output type of an expression. Example: `ls` returns "table",
    /// `1 + 2` returns "int"
    fn infer_output_type(&self, context: &LintContext) -> Option<Type>;
    /// Infers the input type expected by an expression. Example: `$in | length`
    /// expects "list"
    fn infer_input_type(&self, in_var: Option<VarId>, context: &LintContext) -> Option<Type>;
    /// Extracts external command name from expression. Example: `^ls` returns
    /// "ls"
    fn extract_external_command_name(&self, context: &LintContext) -> Option<String>;

    /// Extracts integer value from expression, unwrapping
    /// blocks/subexpressions. Example: `5` returns 5, `(5)` returns 5
    fn extract_int_value(&self, context: &LintContext) -> Option<i64>;
    /// Unwraps block/subexpression to get inner expression.
    /// Example: `(5)` returns the `5` expression
    fn unwrap_block_expr<'a>(&'a self, context: &'a LintContext) -> &'a Self;
    /// Checks if expression is a counter increment pattern.
    /// Example: `$i = $i + 1` or `$i += 1` returns true for `counter_name` "i"
    fn is_counter_increment(&self, counter_name: &str, context: &LintContext) -> bool;

    /// Traverse expression and all descendants with parent tracking.
    /// Calls the callback for each expression with its immediate parent.
    /// The callback returns `ControlFlow::Continue(())` to recurse into
    /// children, or `ControlFlow::Break(())` to skip this expression's
    /// children.
    fn traverse_with_parent<'a, F>(
        &'a self,
        context: &'a LintContext,
        parent: Option<&'a Expression>,
        callback: &mut F,
    ) where
        F: FnMut(&'a Expression, Option<&'a Expression>) -> ControlFlow<()>;
}

pub const fn is_dollar_in_var(var_id: VarId) -> bool {
    use nu_protocol::IN_VARIABLE_ID;
    var_id.get() == IN_VARIABLE_ID.get()
}

const fn has_synthetic_declaration_span(var: &Variable) -> bool {
    (var.declaration_span.start == 0 && var.declaration_span.end == 0)
        || (var.declaration_span.start == var.declaration_span.end
            && var.declaration_span.start > 0)
}

pub fn is_pipeline_input_var(var_id: VarId, context: &LintContext) -> bool {
    use nu_protocol::{ENV_VARIABLE_ID, NU_VARIABLE_ID};

    if is_dollar_in_var(var_id) {
        return true;
    }

    if var_id == ENV_VARIABLE_ID || var_id == NU_VARIABLE_ID {
        return false;
    }

    let var = context.working_set.get_variable(var_id);
    has_synthetic_declaration_span(var)
}

const fn extract_var_from_full_cell_path(cell_path: &FullCellPath) -> Option<VarId> {
    match &cell_path.head.expr {
        Expr::Var(var_id) => Some(*var_id),
        _ => None,
    }
}

const fn builtin_var_name(var_id: VarId) -> Option<&'static str> {
    match var_id.get() {
        id if id == NU_VARIABLE_ID.get() => Some("nu"),
        id if id == IN_VARIABLE_ID.get() => Some("in"),
        id if id == ENV_VARIABLE_ID.get() => Some("env"),
        _ => None,
    }
}

/// Extracts variable name from `Expr::Var`, handling both built-ins and
/// user-defined.
pub fn var_name_from_expr(expr: &Expression, context: &LintContext) -> Option<String> {
    let Expr::Var(var_id) = &expr.expr else {
        return None;
    };

    if let Some(name) = builtin_var_name(*var_id) {
        return Some(name.to_string());
    }

    context
        .span_text(expr.span)
        .strip_prefix('$')
        .map(String::from)
}

impl ExpressionExt for Expression {
    fn extract_variable_name(&self, context: &LintContext) -> Option<String> {
        match &self.expr {
            Expr::Var(var_id) | Expr::VarDecl(var_id) => {
                let var = context.working_set.get_variable(*var_id);
                Some(context.span_text(var.declaration_span).to_string())
            }
            Expr::FullCellPath(cell_path) => cell_path.head.extract_variable_name(context),
            _ => None,
        }
    }

    fn is_assignment(&self) -> bool {
        matches!(
            &self.expr,
            Expr::BinaryOp(_, op, _) if matches!(
                op.expr,
                Expr::Operator(Operator::Assignment(_))
            )
        )
    }

    fn is_empty_list(&self) -> bool {
        match &self.expr {
            Expr::List(list) => list.is_empty(),
            Expr::FullCellPath(cell_path) => cell_path.head.is_empty_list(),
            _ => false,
        }
    }

    fn extract_block_id(&self) -> Option<BlockId> {
        match &self.expr {
            Expr::Block(block_id)
            | Expr::Closure(block_id)
            | Expr::Subexpression(block_id)
            | Expr::RowCondition(block_id) => Some(*block_id),
            // Handle FullCellPath wrapping a subexpression (common parser pattern)
            Expr::FullCellPath(fcp) if fcp.tail.is_empty() => fcp.head.extract_block_id(),
            _ => None,
        }
    }

    fn span_text<'a>(&self, context: &'a LintContext) -> &'a str {
        context.expr_text(self)
    }

    fn extract_assigned_variable(&self) -> Option<VarId> {
        let Expr::BinaryOp(lhs, _op, _rhs) = &self.expr else {
            return None;
        };

        if !self.is_assignment() {
            return None;
        }

        match &lhs.expr {
            Expr::Var(var_id) => Some(*var_id),
            Expr::FullCellPath(cell_path) => extract_var_from_full_cell_path(cell_path),
            _ => None,
        }
    }

    fn extract_field_access(&self, field_name: &str) -> Option<(VarId, Span)> {
        if let Expr::FullCellPath(cell_path) = &self.expr
            && let Some(var_id) = extract_var_from_full_cell_path(cell_path)
            && cell_path.tail.iter().any(|path_member| {
                matches!(
                    path_member,
                    PathMember::String { val, .. } if val == field_name
                )
            })
        {
            Some((var_id, self.span))
        } else {
            None
        }
    }

    fn contains_variables(&self, context: &LintContext) -> bool {
        self.find_map(context.working_set, &|expr| {
            if matches!(&expr.expr, Expr::Var(_) | Expr::VarDecl(_)) {
                FindMapResult::Found(())
            } else {
                FindMapResult::Continue
            }
        })
        .is_some()
    }

    fn is_external_call_with_variable(&self, var_id: VarId) -> bool {
        let Expr::ExternalCall(head, _args) = &self.expr else {
            return false;
        };
        head.matches_var(var_id)
    }

    fn matches_var(&self, var_id: VarId) -> bool {
        match &self.expr {
            Expr::Var(id) => *id == var_id,
            Expr::FullCellPath(cell_path) => {
                extract_var_from_full_cell_path(cell_path) == Some(var_id)
            }
            _ => false,
        }
    }

    fn extract_direct_var(&self) -> Option<VarId> {
        match &self.expr {
            Expr::Var(var_id) => Some(*var_id),
            Expr::FullCellPath(fcp) if fcp.tail.is_empty() => {
                if let Expr::Var(var_id) = &fcp.head.expr {
                    Some(*var_id)
                } else {
                    None
                }
            }
            _ => None,
        }
    }

    fn contains_variable(&self, var_id: VarId) -> bool {
        match &self.expr {
            Expr::Var(id) => *id == var_id,
            Expr::FullCellPath(cell_path) => cell_path.head.contains_variable(var_id),
            Expr::BinaryOp(left, _op, right) => {
                left.contains_variable(var_id) || right.contains_variable(var_id)
            }
            Expr::UnaryNot(inner) => inner.contains_variable(var_id),
            Expr::Call(call) => call.arguments.iter().any(|arg| match arg {
                Argument::Positional(expr)
                | Argument::Named((_, _, Some(expr)))
                | Argument::Unknown(expr)
                | Argument::Spread(expr) => expr.contains_variable(var_id),
                Argument::Named(_) => false,
            }),
            Expr::List(items) => items.iter().any(|item| {
                let expr = match item {
                    ListItem::Item(e) | ListItem::Spread(_, e) => e,
                };
                expr.contains_variable(var_id)
            }),
            Expr::Table(table) => {
                table
                    .columns
                    .iter()
                    .any(|col| col.contains_variable(var_id))
                    || table
                        .rows
                        .iter()
                        .any(|row| row.iter().any(|cell| cell.contains_variable(var_id)))
            }
            Expr::Record(items) => items.iter().any(|item| match item {
                RecordItem::Pair(key, val) => {
                    key.contains_variable(var_id) || val.contains_variable(var_id)
                }
                RecordItem::Spread(_, expr) => expr.contains_variable(var_id),
            }),
            _ => false,
        }
    }

    fn uses_pipeline_input(&self, context: &LintContext) -> bool {
        if matches!(&self.expr, Expr::Collect(..)) {
            return true;
        }

        self.find_pipeline_input(context)
            .is_some_and(|(var_id, _)| {
                let var = context.working_set.get_variable(var_id);
                var.const_val.is_none()
            })
    }

    fn find_pipeline_input(&self, context: &LintContext) -> Option<(VarId, Span)> {
        use super::block::BlockExt;

        match &self.expr {
            Expr::Var(var_id) if is_pipeline_input_var(*var_id, context) => {
                Some((*var_id, self.span))
            }
            Expr::FullCellPath(cell_path) => cell_path.head.find_pipeline_input(context),
            Expr::Call(call) => call.arguments.iter().find_map(|arg| match arg {
                Argument::Positional(e)
                | Argument::Unknown(e)
                | Argument::Named((_, _, Some(e)))
                | Argument::Spread(e) => e.find_pipeline_input(context),
                Argument::Named(_) => None,
            }),
            Expr::BinaryOp(lhs, _, rhs) => lhs
                .find_pipeline_input(context)
                .or_else(|| rhs.find_pipeline_input(context)),
            Expr::UnaryNot(e) | Expr::Collect(_, e) => e.find_pipeline_input(context),
            // Don't recurse into closures - they have their own $in scope
            Expr::Subexpression(block_id) | Expr::Block(block_id) => context
                .working_set
                .get_block(*block_id)
                .find_pipeline_input(context),
            Expr::StringInterpolation(items) => items
                .iter()
                .find_map(|item| item.find_pipeline_input(context)),
            _ => None,
        }
    }

    fn find_dollar_in_usage(&self) -> Option<Span> {
        match &self.expr {
            Expr::Var(var_id) if is_dollar_in_var(*var_id) => Some(self.span),
            Expr::FullCellPath(cell_path) => cell_path.head.find_dollar_in_usage(),
            Expr::Call(call) => call.arguments.iter().find_map(|arg| match arg {
                Argument::Positional(e)
                | Argument::Unknown(e)
                | Argument::Named((_, _, Some(e)))
                | Argument::Spread(e) => e.find_dollar_in_usage(),
                Argument::Named(_) => None,
            }),
            Expr::BinaryOp(lhs, _, rhs) => lhs
                .find_dollar_in_usage()
                .or_else(|| rhs.find_dollar_in_usage()),
            Expr::UnaryNot(e) => e.find_dollar_in_usage(),
            // Collect represents $in usage - return the span of the Collect expression
            Expr::Collect(_, _) => Some(self.span),
            Expr::StringInterpolation(items) => {
                items.iter().find_map(ExpressionExt::find_dollar_in_usage)
            }
            _ => None,
        }
    }

    fn infer_output_type(&self, context: &LintContext) -> Option<Type> {
        let inner_expr = match &self.expr {
            Expr::Collect(_, inner) => &inner.expr,
            _ => &self.expr,
        };
        infer_expr_output_type(inner_expr, &self.ty, context)
    }

    fn infer_input_type(&self, in_var: Option<VarId>, context: &LintContext) -> Option<Type> {
        let in_var_id = in_var?;
        log::trace!(
            "infer_input_type: checking expr='{}', var_id={in_var_id:?}",
            self.span_text(context)
        );

        let result = match &self.expr {
            Expr::FullCellPath(cell_path) if matches!(&cell_path.head.expr, Expr::Var(var_id) if *var_id == in_var_id) =>
            {
                log::trace!(
                    "  -> FullCellPath with matching var, tail_len={}",
                    cell_path.tail.len()
                );
                if !cell_path.tail.is_empty()
                    && cell_path
                        .tail
                        .iter()
                        .any(|member| matches!(member, PathMember::String { .. }))
                {
                    Some(Type::Record(Box::new([])))
                } else if !cell_path.tail.is_empty() {
                    Some(Type::List(Box::new(Type::Any)))
                } else {
                    None
                }
            }
            Expr::FullCellPath(cell_path)
                if matches!(
                    &cell_path.head.expr,
                    Expr::Subexpression(_) | Expr::Block(_) | Expr::Closure(_)
                ) =>
            {
                log::trace!("  -> FullCellPath wrapping block-like expression");
                cell_path.head.infer_input_type(in_var, context)
            }
            Expr::Call(call) => {
                log::trace!("  -> Call expression, checking arguments");
                infer_from_call(call, in_var_id, in_var, context)
            }
            Expr::BinaryOp(left, op_expr, right) => {
                log::trace!("  -> BinaryOp, checking if math/comparison with variable");
                if matches!(&op_expr.expr, Expr::Operator(op) if matches!(op, Operator::Math(_) | Operator::Comparison(_)))
                    && (left.contains_variable(in_var_id) || right.contains_variable(in_var_id))
                {
                    log::trace!("  -> Found math/comparison op with variable, returning Int");
                    return Some(Type::Int);
                }

                log::trace!("  -> Recursing into BinaryOp operands");
                left.infer_input_type(in_var, context)
                    .or_else(|| right.infer_input_type(in_var, context))
            }
            Expr::Collect(_, inner) | Expr::UnaryNot(inner) => {
                log::trace!("  -> Collect/UnaryNot, checking inner");
                inner.infer_input_type(in_var, context)
            }
            Expr::Subexpression(block_id) | Expr::Block(block_id) | Expr::Closure(block_id) => {
                log::trace!("  -> Subexpression/Block/Closure, block_id={block_id:?}");
                let block = context.working_set.get_block(*block_id);
                log::trace!("     Block has {} pipelines", block.pipelines.len());

                let pipeline_type = block
                    .pipelines
                    .iter()
                    .find_map(|pipeline| pipeline.infer_param_type(in_var_id, context));

                if pipeline_type.is_some() {
                    log::trace!("     Found type from pipeline analysis: {pipeline_type:?}");
                    return pipeline_type;
                }

                block
                    .pipelines
                    .iter()
                    .flat_map(|pipeline| &pipeline.elements)
                    .find_map(|element| {
                        log::trace!(
                            "    -> Checking pipeline element, expr='{}', variant={:?}",
                            element.expr.span_text(context),
                            &element.expr.expr
                        );
                        let result = element.expr.infer_input_type(in_var, context);
                        log::trace!("       Result: {result:?}");
                        result
                    })
            }
            Expr::MatchBlock(patterns) => {
                log::trace!("  -> MatchBlock, checking patterns");
                patterns
                    .iter()
                    .find_map(|(_, expr)| expr.infer_input_type(in_var, context))
            }
            _ => {
                log::trace!("  -> No match for expression type");
                None
            }
        };

        log::trace!("infer_input_type result: {result:?}");
        result
    }

    fn extract_external_command_name(&self, context: &LintContext) -> Option<String> {
        use nu_protocol::ast::Traverse;

        self.find_map(context.working_set, &|inner_expr| {
            if let Expr::ExternalCall(cmd_expr, _) = &inner_expr.expr {
                match &cmd_expr.expr {
                    Expr::String(s) => FindMapResult::Found(s.clone()),
                    Expr::GlobPattern(pattern, _) => FindMapResult::Found(pattern.clone()),
                    _ => FindMapResult::Continue,
                }
            } else {
                FindMapResult::Continue
            }
        })
    }

    fn extract_int_value(&self, context: &LintContext) -> Option<i64> {
        match &self.expr {
            Expr::Int(n) => Some(*n),
            Expr::Block(block_id) | Expr::Subexpression(block_id) => {
                let block = context.working_set.get_block(*block_id);
                block
                    .pipelines
                    .first()
                    .and_then(|pipeline| pipeline.elements.first())
                    .and_then(|elem| elem.expr.extract_int_value(context))
            }
            _ => None,
        }
    }

    fn unwrap_block_expr<'a>(&'a self, context: &'a LintContext) -> &'a Self {
        match &self.expr {
            Expr::Block(block_id) | Expr::Subexpression(block_id) => {
                let block = context.working_set.get_block(*block_id);
                block
                    .pipelines
                    .first()
                    .and_then(|pipeline| pipeline.elements.first())
                    .map_or(self, |elem| &elem.expr)
            }
            _ => self,
        }
    }

    fn is_counter_increment(&self, counter_name: &str, context: &LintContext) -> bool {
        use nu_protocol::ast::{Assignment, Math};

        let Expr::BinaryOp(lhs, op, rhs) = &self.expr else {
            return false;
        };

        let Expr::Operator(Operator::Assignment(assignment_op)) = &op.expr else {
            return false;
        };

        if lhs.extract_variable_name(context).as_deref() != Some(counter_name) {
            return false;
        }

        let is_add_one = |left: &Self, op: &Self, right: &Self| -> bool {
            left.extract_variable_name(context).as_deref() == Some(counter_name)
                && matches!(&op.expr, Expr::Operator(Operator::Math(Math::Add)))
                && matches!(&right.expr, Expr::Int(1))
        };

        match assignment_op {
            Assignment::Assign => {
                let rhs_unwrapped = rhs.unwrap_block_expr(context);
                matches!(
                    &rhs_unwrapped.expr,
                    Expr::BinaryOp(add_left, add_op, add_right)
                        if is_add_one(add_left, add_op, add_right)
                )
            }
            Assignment::AddAssign => rhs.extract_int_value(context) == Some(1),
            _ => false,
        }
    }

    #[allow(clippy::excessive_nesting, reason = "Recursive")]
    fn traverse_with_parent<'a, F>(
        &'a self,
        context: &'a LintContext,
        parent: Option<&'a Expression>,
        callback: &mut F,
    ) where
        F: FnMut(&'a Self, Option<&'a Self>) -> ControlFlow<()>,
    {
        // Call callback for this expression - if it returns Break, skip children
        if callback(self, parent).is_break() {
            return;
        }

        // Recursively process children, marking self as their parent
        let mut recur = |child: &'a Self| {
            child.traverse_with_parent(context, Some(self), callback);
        };

        // This structure mirrors Expression::flat_map from nu_protocol::ast::Traverse
        // to ensure it stays in sync with upstream changes
        match &self.expr {
            Expr::RowCondition(block_id)
            | Expr::Subexpression(block_id)
            | Expr::Block(block_id)
            | Expr::Closure(block_id) => {
                let block = context.working_set.get_block(*block_id);
                block.traverse_with_parent(context, Some(self), callback);
            }
            Expr::Range(range) => {
                for sub_expr in [&range.from, &range.next, &range.to].into_iter().flatten() {
                    recur(sub_expr);
                }
            }
            Expr::Call(call) => {
                for arg in &call.arguments {
                    if let Some(sub_expr) = arg.expr() {
                        recur(sub_expr);
                    }
                }
            }
            Expr::ExternalCall(head, args) => {
                recur(head.as_ref());
                for arg in args {
                    recur(arg.expr());
                }
            }
            Expr::UnaryNot(e) | Expr::Collect(_, e) => recur(e.as_ref()),
            Expr::BinaryOp(lhs, op, rhs) => {
                recur(lhs);
                recur(op);
                recur(rhs);
            }
            Expr::MatchBlock(matches) => {
                for (_pattern, e) in matches {
                    recur(e);
                }
            }
            Expr::List(items) => {
                for item in items {
                    match item {
                        ListItem::Item(e) | ListItem::Spread(_, e) => recur(e),
                    }
                }
            }
            Expr::Record(items) => {
                for item in items {
                    match item {
                        RecordItem::Spread(_, e) => recur(e),
                        RecordItem::Pair(key, val) => {
                            recur(key);
                            recur(val);
                        }
                    }
                }
            }
            Expr::Table(table) => {
                for column in &table.columns {
                    recur(column);
                }
                for row in &table.rows {
                    for item in row {
                        recur(item);
                    }
                }
            }
            Expr::ValueWithUnit(vu) => recur(&vu.expr),
            Expr::FullCellPath(fcp) => recur(&fcp.head),
            Expr::Keyword(kw) => recur(&kw.expr),
            Expr::StringInterpolation(vec) | Expr::GlobInterpolation(vec, _) => {
                for item in vec {
                    recur(item);
                }
            }
            Expr::AttributeBlock(ab) => {
                for attr in &ab.attributes {
                    recur(&attr.expr);
                }
                recur(&ab.item);
            }
            _ => (),
        }
    }
}

fn infer_from_call(
    call: &Call,
    in_var_id: VarId,
    in_var: Option<VarId>,
    context: &LintContext,
) -> Option<Type> {
    log::trace!("infer_from_call: checking call for var_id={in_var_id:?}");

    for (idx, arg) in call.arguments.iter().enumerate() {
        if let Argument::Positional(arg_expr) | Argument::Unknown(arg_expr) = arg {
            log::trace!("  -> Checking positional arg {idx}");
            if !arg_expr.contains_variable(in_var_id) {
                log::trace!("    -> Does not contain variable");
                continue;
            }

            log::trace!("    -> Contains variable! Checking signature");
            let decl = context.working_set.get_decl(call.decl_id);
            let signature = decl.signature();

            log::trace!(
                "    -> Command: '{}', input_output_types: {:?}",
                decl.name(),
                signature.input_output_types
            );

            if let Some((input_type, _)) = signature.input_output_types.first()
                && !matches!(input_type, nu_protocol::Type::Any)
            {
                log::trace!("    -> Found input type from signature: {input_type:?}");
                return Some(input_type.clone());
            }
            log::trace!("    -> Signature has no specific input type");
        }
    }

    log::trace!("  -> Recursively checking call arguments");
    let result = call.arguments.iter().find_map(|arg| match arg {
        Argument::Positional(arg_expr) | Argument::Unknown(arg_expr) => {
            arg_expr.infer_input_type(in_var, context)
        }
        _ => None,
    });

    log::trace!("infer_from_call result: {result:?}");
    result
}

const fn is_filepath_expr(expr: &Expr) -> bool {
    matches!(expr, Expr::Filepath(..))
}

const fn is_glob_pattern_expr(expr: &Expr) -> bool {
    matches!(expr, Expr::GlobPattern(..))
}

fn check_filepath_output(expr: &Expr) -> Option<Type> {
    let ty = Type::Custom("path".into());
    match expr {
        Expr::ExternalCall(head, _) if matches!(&head.expr, Expr::Filepath(..)) => {
            log::trace!(
                "check_filepath_output: ExternalCall with filepath head: {:?}",
                head.expr
            );
            Some(ty)
        }
        Expr::Collect(_, inner) if is_filepath_expr(&inner.expr) => {
            log::trace!("check_filepath_output: Collect with filepath inner");
            Some(ty)
        }
        expr if is_filepath_expr(expr) || is_glob_pattern_expr(expr) => {
            log::trace!("check_filepath_output: filepath expr: {expr:?}");
            Some(ty)
        }
        _ => None,
    }
}

fn infer_expr_output_type(expr: &Expr, ty: &Type, context: &LintContext) -> Option<Type> {
    match expr {
        expr if check_filepath_output(expr).is_some() => check_filepath_output(expr),
        Expr::Bool(_)
        | Expr::Int(_)
        | Expr::Float(_)
        | Expr::String(_)
        | Expr::StringInterpolation(_)
        | Expr::RawString(_)
        | Expr::Record(_)
        | Expr::Table(_) => Some(ty.clone()),
        Expr::BinaryOp(left, _op, right) => {
            if !matches!(ty, Type::Any) {
                return Some(ty.clone());
            }
            infer_binary_op_type(
                left.infer_output_type(context).as_ref(),
                right.infer_output_type(context).as_ref(),
            )
            .or_else(|| Some(ty.clone()))
        }
        Expr::List(items) => Some(infer_list_element_type(items)),
        Expr::Nothing => Some(Type::Nothing),
        Expr::FullCellPath(path) => {
            if let Expr::List(items) = &path.head.expr {
                return Some(infer_list_element_type(items));
            }
            if !path.tail.is_empty() {
                return Some(path.head.ty.clone());
            }
            path.head
                .infer_output_type(context)
                .or_else(|| Some(path.head.ty.clone()))
        }
        Expr::Subexpression(block_id) | Expr::Block(block_id) => Some(
            context
                .working_set
                .get_block(*block_id)
                .infer_output_type(context),
        ),
        Expr::ExternalCall(call, args) => {
            let cmd_name = context.expr_text(call);
            if has_external_side_effect(cmd_name, ExternEffect::NoDataInStdout, context, args) {
                Some(Type::Nothing)
            } else {
                Some(Type::String)
            }
        }
        Expr::Call(call) => {
            let decl = context.working_set.get_decl(call.decl_id);
            let cmd_name = decl.name();
            if matches!(cmd_name, "if" | "match" | "try" | "do")
                && let Some(unified_type) = call.infer_from_blocks(context)
            {
                return Some(unified_type);
            }
            Some(call.get_output_type(context, None))
        }
        Expr::Var(var_id) => {
            let var = context.working_set.get_variable(*var_id);
            if matches!(var.ty, Type::Any) {
                None
            } else {
                Some(var.ty.clone())
            }
        }
        _ => None,
    }
}

const fn infer_binary_op_type(left: Option<&Type>, right: Option<&Type>) -> Option<Type> {
    match (left, right) {
        (Some(Type::Float), _) | (_, Some(Type::Float)) => Some(Type::Float),
        (Some(Type::Int | Type::Any), Some(Type::Int)) | (Some(Type::Int), Some(Type::Any)) => {
            Some(Type::Int)
        }
        _ => None,
    }
}

fn infer_list_element_type(items: &[ListItem]) -> Type {
    if items.is_empty() {
        return Type::List(Box::new(Type::Any));
    }

    let element_types: Vec<Type> = items
        .iter()
        .map(|item| match item {
            ListItem::Item(expr) | ListItem::Spread(_, expr) => expr.ty.clone(),
        })
        .collect();

    if element_types.is_empty() {
        return Type::List(Box::new(Type::Any));
    }

    if element_types.iter().all(|t| t == &element_types[0]) {
        log::trace!("All list elements have type: {:?}", element_types[0]);
        Type::List(Box::new(element_types[0].clone()))
    } else {
        log::trace!("List has mixed types, using Any");
        Type::List(Box::new(Type::Any))
    }
}
