use diagnostics::LocalSink;
use syntax::ast::{Expression, Span, StructKind};
use syntax::program::{Definition, DefinitionBody};
use syntax::types::{Symbol, Type, unqualified_name};

use crate::store::Store;

pub(crate) fn run(typed_ast: &[Expression], module_id: &str, store: &Store, sink: &LocalSink) {
    for item in typed_ast {
        visit_expression(item, false, false, module_id, store, sink);
    }
}

fn visit_expression(
    expression: &Expression,
    is_callee: bool,
    is_dot_access_base: bool,
    module_id: &str,
    store: &Store,
    sink: &LocalSink,
) {
    if let Expression::Identifier {
        value, ty, span, ..
    } = expression
        && !is_callee
    {
        check_one(value, ty, *span, is_dot_access_base, module_id, store, sink);
    }

    match expression {
        Expression::Call {
            expression: callee,
            args,
            spread,
            ..
        } => {
            visit_expression(callee, true, false, module_id, store, sink);
            for arg in args {
                visit_expression(arg, false, false, module_id, store, sink);
            }
            if let Some(s) = spread.as_ref() {
                visit_expression(s, false, false, module_id, store, sink);
            }
        }
        Expression::Paren {
            expression: inner, ..
        } => {
            visit_expression(inner, is_callee, is_dot_access_base, module_id, store, sink);
        }
        Expression::DotAccess {
            expression: inner, ..
        } => {
            visit_expression(inner, false, true, module_id, store, sink);
        }
        _ => {
            for child in expression.children() {
                visit_expression(child, false, false, module_id, store, sink);
            }
        }
    }
}

fn check_one(
    value: &str,
    ty: &Type,
    span: Span,
    is_dot_access_base: bool,
    module_id: &str,
    store: &Store,
    sink: &LocalSink,
) {
    if matches!(
        value,
        "imaginary" | "assert_type" | "complex" | "real" | "panic"
    ) {
        let qualified = Symbol::from_parts(module_id, value);
        if store.get_definition(&qualified).is_none() {
            sink.push(diagnostics::infer::native_constructor_value(value, span));
            return;
        }
    }

    {
        let qualified = if value.contains('.') {
            value.to_string()
        } else {
            Symbol::from_parts(module_id, value).to_string()
        };
        if resolves_to_struct_kind(&qualified, StructKind::Tuple, store) {
            sink.push(diagnostics::infer::native_constructor_value(value, span));
            return;
        }
        if !is_dot_access_base && resolves_to_struct_kind(&qualified, StructKind::Record, store) {
            sink.push(diagnostics::infer::record_struct_value(value, span));
            return;
        }
    }

    let Some((type_part, method_part)) = value.split_once('.') else {
        return;
    };
    if method_part.contains('.') {
        return;
    }

    let is_native = matches!(
        type_part,
        "Slice" | "EnumeratedSlice" | "Map" | "Channel" | "Sender" | "Receiver" | "string"
    );

    if is_native {
        if matches!(method_part, "new" | "buffered") {
            sink.push(diagnostics::infer::native_constructor_value(value, span));
        } else {
            sink.push(diagnostics::infer::native_method_value(
                method_part,
                diagnostics::infer::NativeMethodForm::Static,
                span,
            ));
        }
        return;
    }

    if matches!(method_part, "new" | "buffered") {
        let ret_ty = match ty {
            Type::Function(f) => Some(f.return_type.as_ref()),
            Type::Forall { body, .. } => match body.as_ref() {
                Type::Function(f) => Some(f.return_type.as_ref()),
                _ => None,
            },
            _ => None,
        };
        if let Some(ret) = ret_ty {
            let is_native_ret = matches!(ret.get_name(), Some("Channel" | "Map" | "Slice"));
            if is_native_ret {
                sink.push(diagnostics::infer::native_constructor_value(value, span));
                return;
            }
        }
    }

    let is_fn = matches!(ty, Type::Function(_) | Type::Forall { .. });
    if !is_fn {
        return;
    }
    let fn_params = match ty {
        Type::Function(f) => f.params.as_slice(),
        Type::Forall { body, .. } => match body.as_ref() {
            Type::Function(f) => f.params.as_slice(),
            _ => return,
        },
        _ => return,
    };
    let Some(first) = fn_params.first() else {
        return;
    };
    let stripped = first.strip_refs();
    let is_self = matches!(&stripped, Type::Nominal { id, .. }
        if unqualified_name(id) == type_part);
    if !is_self {
        return;
    }

    let method_key = format!("{}.{}.{}", module_id, type_part, method_part);
    let is_public = store
        .get_definition(&method_key)
        .map(|d| d.visibility().is_public())
        .unwrap_or(true);

    if !is_public {
        sink.push(diagnostics::infer::private_method_expression(span));
    }
}

fn resolves_to_struct_kind(qualified: &str, kind: StructKind, store: &Store) -> bool {
    if let Some(k) = store.struct_kind(qualified) {
        return k == kind;
    }
    match store.get_definition(qualified) {
        Some(Definition {
            ty: alias_ty,
            body: DefinitionBody::TypeAlias { .. },
            ..
        }) => {
            if let Type::Nominal { id, .. } = alias_ty.unwrap_forall() {
                store.struct_kind(id) == Some(kind)
            } else {
                false
            }
        }
        _ => false,
    }
}
