//! Routines and types to list all members present on a given type or in a given scope.
//!
//! These two concepts are closely related, since listing all members of a given
//! module-literal type requires listing all members in the module's scope, and
//! listing all members on a nominal-instance type or a class-literal type requires
//! listing all members in the class's body scope.

use std::cmp::Ordering;

use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet;

use crate::{
    Db,
    place::{
        DefinedPlace, Place, PlaceWithDefinition, imported_symbol, place_from_bindings,
        place_from_declarations,
    },
    types::{
        ClassBase, ClassLiteral, KnownClass, ProgramEnvironment, StaticClassLiteral,
        SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind,
        exists_at_runtime,
    },
};
use ty_python_core::{
    ProgramFile, attribute_scopes, definition::Definition, global_scope, place_table,
    scope::ScopeId, semantic_index, use_def_map,
};

/// Iterate over all declarations and bindings that exist at the end
/// of the given scope.
pub(crate) fn all_end_of_scope_members<'db>(
    db: &'db dyn Db,
    scope_id: ScopeId<'db>,
) -> impl Iterator<Item = MemberWithDefinition<'db>> + 'db {
    let env = ProgramEnvironment::from_scope(scope_id);

    let use_def_map = use_def_map(db, scope_id);
    let table = place_table(db, scope_id);
    let bindings_ctx = env.clone();

    use_def_map
        .all_end_of_scope_symbol_declarations()
        .filter_map(move |(symbol_id, declarations)| {
            let place_result = place_from_declarations(db, &env, declarations);
            let first_reachable_definition = place_result.first_declaration?;
            let ty = place_result
                .ignore_conflicting_declarations()
                .place
                .ignore_possibly_undefined()?;
            let symbol = table.symbol(symbol_id);
            let member = Member {
                name: symbol.name().clone(),
                ty,
                is_type_check_only: false,
            };
            Some(MemberWithDefinition {
                member,
                first_reachable_definition,
            })
        })
        .chain(use_def_map.all_end_of_scope_symbol_bindings().filter_map(
            move |(symbol_id, bindings)| {
                let PlaceWithDefinition {
                    place,
                    first_definition,
                } = place_from_bindings(db, &bindings_ctx, bindings);

                let first_reachable_definition = first_definition?;
                let ty = place.ignore_possibly_undefined()?;

                let symbol = table.symbol(symbol_id);
                let member = Member {
                    name: symbol.name().clone(),
                    ty,
                    is_type_check_only: false,
                };
                Some(MemberWithDefinition {
                    member,
                    first_reachable_definition,
                })
            },
        ))
}

/// Iterate over all declarations and bindings that are reachable anywhere
/// in the given scope.
pub(crate) fn all_reachable_members<'db>(
    db: &'db dyn Db,
    scope_id: ScopeId<'db>,
) -> impl Iterator<Item = MemberWithDefinition<'db>> + 'db {
    let env = ProgramEnvironment::from_scope(scope_id);

    let use_def_map = use_def_map(db, scope_id);
    let table = place_table(db, scope_id);

    use_def_map
        .all_reachable_symbols()
        .flat_map(move |(symbol_id, declarations, bindings)| {
            let symbol = table.symbol(symbol_id);

            let declaration_place_result = place_from_declarations(db, &env, declarations);
            let declaration =
                declaration_place_result
                    .first_declaration
                    .and_then(|first_reachable_definition| {
                        let ty = declaration_place_result
                            .ignore_conflicting_declarations()
                            .place
                            .ignore_possibly_undefined()?;
                        let member = Member {
                            name: symbol.name().clone(),
                            ty,
                            is_type_check_only: false,
                        };
                        Some(MemberWithDefinition {
                            member,
                            first_reachable_definition,
                        })
                    });

            let place_with_definition = place_from_bindings(db, &env, bindings);
            let binding =
                place_with_definition
                    .first_definition
                    .and_then(|first_reachable_definition| {
                        let ty = place_with_definition.place.ignore_possibly_undefined()?;
                        let member = Member {
                            name: symbol.name().clone(),
                            ty,
                            is_type_check_only: false,
                        };
                        Some(MemberWithDefinition {
                            member,
                            first_reachable_definition,
                        })
                    });

            [declaration, binding]
        })
        .flatten()
}

// `__init__`, `__repr__`, `__eq__`, `__ne__` and `__hash__` are always included via `object`,
// so we don't need to list them here.
const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[
    "__lt__",
    "__le__",
    "__gt__",
    "__ge__",
    "__replace__",
    "__setattr__",
    "__delattr__",
    "__slots__",
    "__weakref__",
    "__match_args__",
    "__dataclass_fields__",
    "__dataclass_params__",
];

struct AllMembers<'db> {
    members: FxHashSet<Member<'db>>,
}

impl<'db> AllMembers<'db> {
    fn of(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Self {
        let mut all_members = Self {
            members: FxHashSet::default(),
        };
        all_members.extend_with_type(db, env, ty);
        all_members
    }

    fn extend_with_type(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) {
        match ty {
            Type::Union(union) => {
                fn is_dynamic(db: &dyn Db, ty: Type<'_>) -> bool {
                    // We don't need to use recursion here because
                    // `Type` guarantees that unions/intersections
                    // are kept in DNF (i.e., they are flattened).
                    ty.is_dynamic()
                        || match ty {
                            Type::Intersection(intersection) => {
                                intersection.positive(db).iter().any(Type::is_dynamic)
                            }
                            _ => false,
                        }
                }

                let union = match union.filter(db, |&ty| !is_dynamic(db, ty)) {
                    Type::Union(union) => union,
                    ty => return self.extend_with_type(db, env, ty),
                };
                self.members.extend(
                    union
                        .elements(db)
                        .iter()
                        .map(|ty| AllMembers::of(db, env, *ty).members)
                        .reduce(|acc, members| acc.intersection(&members).cloned().collect())
                        .unwrap_or_default(),
                );
            }

            Type::Intersection(intersection) => self.members.extend(
                intersection
                    .positive(db)
                    .iter()
                    .map(|ty| AllMembers::of(db, env, *ty).members)
                    .reduce(|acc, members| acc.union(&members).cloned().collect())
                    .unwrap_or_default(),
            ),

            Type::EnumComplement(complement) => {
                self.extend_with_type(db, env, complement.to_intersection(db, env));
            }

            Type::NominalInstance(instance) => {
                let class = instance.class(db, env);
                if let Some((class_literal, _)) = class.static_class_literal(db) {
                    self.extend_with_instance_members(db, env, ty, class_literal);
                    self.extend_with_synthetic_members(
                        db,
                        env,
                        ty,
                        ClassLiteral::Static(class_literal),
                    );
                } else {
                    // For dynamic classes, we can't enumerate instance members (requires body scope),
                    // but we can still add synthetic members for dataclass-like classes.
                    self.extend_with_synthetic_members(db, env, ty, class.class_literal(db));
                }
            }

            Type::NewTypeInstance(newtype) => {
                self.extend_with_type(db, env, newtype.concrete_base_type(db));
            }

            Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => {
                self.extend_with_type(
                    db,
                    env,
                    KnownClass::TypedDictFallback.to_class_literal(db, env),
                );
            }

            Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => {
                self.extend_with_type(
                    db,
                    env,
                    KnownClass::TypedDictFallback.to_class_literal(db, env),
                );
            }

            Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db, env) => {
                self.extend_with_type(
                    db,
                    env,
                    KnownClass::TypedDictFallback.to_class_literal(db, env),
                );
            }

            Type::ClassLiteral(class_literal) => {
                self.extend_with_class_members(db, env, ty, class_literal);
                self.extend_with_synthetic_members(db, env, ty, class_literal);
                self.extend_with_metaclass_members(db, env, ty, class_literal.metaclass(db));
            }

            Type::GenericAlias(generic_alias) => {
                let class_literal = generic_alias.origin(db);
                self.extend_with_class_members(db, env, ty, ClassLiteral::Static(class_literal));
                self.extend_with_synthetic_members(
                    db,
                    env,
                    ty,
                    ClassLiteral::Static(class_literal),
                );
                self.extend_with_metaclass_members(db, env, ty, class_literal.metaclass(db));
            }

            Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
                SubclassOfInner::Dynamic(_) => {
                    self.extend_with_type(db, env, KnownClass::Type.to_instance(db, env));
                }
                SubclassOfInner::Protocol(protocol) => {
                    if let Some((class_literal, _)) = protocol
                        .class_origin(db)
                        .and_then(|origin| origin.static_class_literal(db))
                    {
                        self.extend_with_class_members(
                            db,
                            env,
                            ty,
                            ClassLiteral::Static(class_literal),
                        );
                        self.extend_with_synthetic_members(
                            db,
                            env,
                            ty,
                            ClassLiteral::Static(class_literal),
                        );
                    }
                    // A structural implementation can use any metaclass, so only members of
                    // `type` itself are guaranteed in addition to the protocol interface.
                    self.extend_with_type(db, env, KnownClass::Type.to_instance(db, env));
                }
                _ => {
                    if let Some(class_type) = subclass_of_type.subclass_of().into_class(db, env)
                        && let Some((class_literal, _)) = class_type.static_class_literal(db)
                    {
                        let static_class = ClassLiteral::Static(class_literal);
                        self.extend_with_class_members(db, env, ty, static_class);
                        self.extend_with_synthetic_members(db, env, ty, static_class);
                        self.extend_with_metaclass_members(
                            db,
                            env,
                            ty,
                            class_literal.metaclass(db),
                        );
                    }
                }
            },

            Type::Dynamic(_)
            | Type::Divergent(_)
            | Type::Never
            | Type::AlwaysTruthy
            | Type::AlwaysFalsy
            | Type::TypeForm(_) => {
                self.extend_with_type(db, env, Type::object());
            }

            Type::TypeAlias(alias) => {
                self.extend_with_type(db, env, alias.value_type(db));
            }

            Type::TypeVar(bound_typevar) => {
                match bound_typevar.typevar(db).bound_or_constraints(db, env) {
                    None => {
                        self.extend_with_type(db, env, Type::object());
                    }
                    Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
                        self.extend_with_type(db, env, bound);
                    }
                    Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
                        self.members.extend(
                            constraints
                                .elements(db)
                                .iter()
                                .map(|ty| AllMembers::of(db, env, *ty).members)
                                .reduce(|acc, members| {
                                    acc.intersection(&members).cloned().collect()
                                })
                                .unwrap_or_default(),
                        );
                    }
                }
            }

            Type::LiteralValue(_)
            | Type::PropertyInstance(_)
            | Type::FunctionLiteral(_)
            | Type::BoundMethod(_)
            | Type::KnownBoundMethod(_)
            | Type::WrapperDescriptor(_)
            | Type::DataclassDecorator(_)
            | Type::DataclassTransformer(_)
            | Type::Callable(_)
            | Type::ProtocolInstance(_)
            | Type::SpecialForm(_)
            | Type::KnownInstance(_)
            | Type::BoundSuper(_)
            | Type::TypeIs(_)
            | Type::TypeGuard(_) => match ty.to_meta_type(db, env) {
                Type::ClassLiteral(class_literal) => {
                    self.extend_with_class_members(db, env, ty, class_literal);
                }
                Type::SubclassOf(subclass_of) => {
                    if let Some(class) = subclass_of.subclass_of().into_class(db, env)
                        && let Some((class_literal, _)) = class.static_class_literal(db)
                    {
                        self.extend_with_class_members(
                            db,
                            env,
                            ty,
                            ClassLiteral::Static(class_literal),
                        );
                    }
                }
                Type::GenericAlias(generic_alias) => {
                    let class_literal = generic_alias.origin(db);
                    self.extend_with_class_members(
                        db,
                        env,
                        ty,
                        ClassLiteral::Static(class_literal),
                    );
                }
                _ => {}
            },

            Type::TypedDict(_) => {
                if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db, env) {
                    self.extend_with_class_members(db, env, ty, class_literal);
                }

                if let Type::ClassLiteral(ClassLiteral::Static(class)) =
                    KnownClass::TypedDictFallback.to_class_literal(db, env)
                {
                    self.extend_with_instance_members(db, env, ty, class);
                }
            }

            Type::ModuleLiteral(literal) => {
                let module = literal.module(db);
                // Looking up `__file__` on `types.ModuleType` will not give as precise a type
                // as we infer in type inference, but it's confusing if autocomplete etc.
                // shows a different type in the tooltip to the one inferred by the type checker.
                let dunder_file_type = if module.file(db).is_some() {
                    KnownClass::Str.to_instance(db, env)
                } else {
                    Type::none(db, env)
                };
                self.members.insert(Member {
                    name: Name::new_static("__file__"),
                    ty: dunder_file_type,
                    is_type_check_only: false,
                });

                self.extend_with_type(db, env, KnownClass::ModuleType.to_instance(db, env));

                let Some(file) = module.file(db) else {
                    return;
                };
                let program_file = ProgramFile::new(db, file, env.program(db));

                let module_scope = global_scope(db, program_file);
                let use_def_map = use_def_map(db, module_scope);
                let place_table = place_table(db, module_scope);

                for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() {
                    let symbol_name = place_table.symbol(symbol_id).name();
                    let Place::Defined(defined) =
                        imported_symbol(db, env, Some(program_file), symbol_name, None).place
                    else {
                        continue;
                    };

                    self.members.insert(Member {
                        name: symbol_name.clone(),
                        ty: defined.ty,
                        is_type_check_only: defined
                            .provenance
                            .definition()
                            .is_some_and(|definition| !exists_at_runtime(db, definition)),
                    });
                }

                self.members
                    .extend(literal.available_submodule_attributes(db).filter_map(
                        |submodule_name| {
                            let ty = literal.resolve_submodule(db, &submodule_name)?;
                            let name = submodule_name.clone();
                            Some(Member {
                                name,
                                ty,
                                is_type_check_only: false,
                            })
                        },
                    ));
            }
        }
    }

    /// Add members from `class_literal` (including following its
    /// parent classes).
    ///
    /// `ty` should be the original type that we're adding members for.
    /// For example, in:
    ///
    /// ```text
    /// class Meta(type):
    ///     @property
    ///     def meta_attr(self) -> int:
    ///         return 0
    ///
    /// class C(metaclass=Meta): ...
    ///
    /// C.<CURSOR>
    /// ```
    ///
    /// then `class_literal` might be `Meta`, but `ty` should be the
    /// type of `C`. This ensures that the descriptor protocol is
    /// correctly used (or not used) to get the type of each member of
    /// `C`.
    fn extend_with_class_members(
        &mut self,
        db: &'db dyn Db,
        env: &ProgramEnvironment<'db>,
        ty: Type<'db>,
        class_literal: ClassLiteral<'db>,
    ) {
        for parent in class_literal
            .iter_mro(db)
            .filter_map(ClassBase::into_class)
            .filter_map(|class| class.static_class_literal(db).map(|(lit, _)| lit))
        {
            let parent_scope = parent.body_scope(db);
            for memberdef in all_end_of_scope_members(db, parent_scope) {
                let result = ty.member(db, env, memberdef.member.name.as_str());
                let Some(ty) = result.place.ignore_possibly_undefined() else {
                    continue;
                };
                self.members.insert(Member {
                    name: memberdef.member.name,
                    ty,
                    is_type_check_only: memberdef.member.is_type_check_only,
                });
            }
        }
    }

    /// Extend a class object's members with members set by its metaclass.
    ///
    /// A static metaclass can also assign attributes onto the class objects that it creates,
    /// so those implicit instance members are available on the class object as well.
    fn extend_with_metaclass_members(
        &mut self,
        db: &'db dyn Db,
        env: &ProgramEnvironment<'db>,
        ty: Type<'db>,
        metaclass: Type<'db>,
    ) {
        let Some(metaclass) = metaclass.to_class_type(db) else {
            return;
        };

        self.extend_with_class_members(db, env, ty, metaclass.class_literal(db));
        if let Some((metaclass, _)) = metaclass.static_class_literal(db) {
            self.extend_with_instance_members(db, env, ty, metaclass);
        }
    }

    /// Extend with instance members from a single class (not its MRO).
    fn extend_with_instance_members_for_class(
        &mut self,
        db: &'db dyn Db,
        env: &ProgramEnvironment<'db>,
        ty: Type<'db>,
        class_literal: StaticClassLiteral<'db>,
    ) {
        let class_body_scope = class_literal.body_scope(db);
        let program_file = class_body_scope.program_file(db);
        let index = semantic_index(db, program_file);
        for function_scope_id in attribute_scopes(db, class_body_scope) {
            for place_expr in index.place_table(function_scope_id).members() {
                let Some(name) = place_expr.as_instance_attribute() else {
                    continue;
                };
                let result = ty.member(db, env, name);
                let Some(ty) = result.place.ignore_possibly_undefined() else {
                    continue;
                };
                self.members.insert(Member {
                    name: Name::new(name),
                    ty,
                    is_type_check_only: false,
                });
            }
        }

        // This is very similar to `extend_with_class_members`,
        // but uses the type of the class instance to query the
        // class member. This gets us the right type for each
        // member, e.g., `SomeClass.__delattr__` is not a bound
        // method, but `instance_of_SomeClass.__delattr__` is.
        for memberdef in all_end_of_scope_members(db, class_body_scope) {
            let result = ty.member(db, env, memberdef.member.name.as_str());
            let Some(ty) = result.place.ignore_possibly_undefined() else {
                continue;
            };
            self.members.insert(Member {
                name: memberdef.member.name,
                ty,
                is_type_check_only: memberdef.member.is_type_check_only,
            });
        }
    }

    /// Extend with instance members from a class and all classes in its MRO.
    fn extend_with_instance_members(
        &mut self,
        db: &'db dyn Db,
        env: &ProgramEnvironment<'db>,
        ty: Type<'db>,
        class_literal: StaticClassLiteral<'db>,
    ) {
        for class in class_literal
            .iter_mro(db, None)
            .filter_map(ClassBase::into_class)
        {
            if let Some((class_literal, _)) = class.static_class_literal(db) {
                self.extend_with_instance_members_for_class(db, env, ty, class_literal);
            }
        }
    }

    fn extend_with_synthetic_members(
        &mut self,
        db: &'db dyn Db,
        env: &ProgramEnvironment<'db>,
        ty: Type<'db>,
        class_literal: ClassLiteral<'db>,
    ) {
        match CodeGeneratorKind::from_class(db, class_literal) {
            Some(CodeGeneratorKind::NamedTuple) => {
                if ty.is_nominal_instance() {
                    self.extend_with_type(
                        db,
                        env,
                        KnownClass::NamedTupleFallback.to_instance(db, env),
                    );
                } else {
                    self.extend_with_type(
                        db,
                        env,
                        KnownClass::NamedTupleFallback.to_class_literal(db, env),
                    );
                }
            }
            Some(CodeGeneratorKind::TypedDict) => {}
            Some(kind @ (CodeGeneratorKind::DataclassLike(_) | CodeGeneratorKind::Pydantic(_))) => {
                let synthetic_attributes: &[&str] = if kind.is_pydantic() {
                    &["__replace__"]
                } else {
                    SYNTHETIC_DATACLASS_ATTRIBUTES
                };

                for attr in synthetic_attributes {
                    if let Place::Defined(DefinedPlace {
                        ty: synthetic_member,
                        ..
                    }) = ty.member(db, env, attr).place
                    {
                        self.members.insert(Member {
                            name: Name::from(*attr),
                            ty: synthetic_member,
                            is_type_check_only: false,
                        });
                    }
                }
            }
            None => {}
        }
    }
}

/// A member of a type or scope, with the first reachable definition of that member.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct MemberWithDefinition<'db> {
    pub(crate) member: Member<'db>,
    pub(crate) first_reachable_definition: Definition<'db>,
}

/// A member of a type or scope.
///
/// In the context of the `all_members` routine, this represents
/// a single item in (ideally) the list returned by `dir(object)`.
///
/// The equality, comparison and hashing traits implemented for
/// this type are done so by taking only the name into account. At
/// present, this is because we assume the name is enough to uniquely
/// identify each attribute on an object. This is perhaps complicated
/// by overloads, but they only get represented by one member for
/// now. Moreover, it is convenient to be able to sort collections of
/// members, and a [`Type`] currently (as of 2025-07-09) has no way to do
/// ordered comparisons.
#[derive(Clone, Debug)]
pub struct Member<'db> {
    pub(crate) name: Name,
    pub(crate) ty: Type<'db>,
    /// Whether this member is known to exist only during type checking.
    pub(crate) is_type_check_only: bool,
}

impl std::hash::Hash for Member<'_> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

impl Eq for Member<'_> {}

impl<'db> PartialEq for Member<'db> {
    fn eq(&self, rhs: &Member<'db>) -> bool {
        self.name == rhs.name
    }
}

impl<'db> Ord for Member<'db> {
    fn cmp(&self, rhs: &Member<'db>) -> Ordering {
        self.name.cmp(&rhs.name)
    }
}

impl<'db> PartialOrd for Member<'db> {
    fn partial_cmp(&self, rhs: &Member<'db>) -> Option<Ordering> {
        Some(self.cmp(rhs))
    }
}

/// List all members of a given type: anything that would be valid when accessed
/// as an attribute on an object of the given type.
pub(crate) fn all_members<'db>(
    db: &'db dyn Db,
    env: &ProgramEnvironment<'db>,
    ty: Type<'db>,
) -> FxHashSet<Member<'db>> {
    AllMembers::of(db, env, ty).members
}
