use super::helpers::extract_string_from_expr;
use super::ExpressionAnalyzer;
use crate::context::Context;
use crate::symbol::SymbolKind;
use mir_issues::{IssueKind, Severity};
use mir_types::{Atomic, Union};
use php_ast::ast::{ExprKind, NewExpr, PropertyAccessExpr, StaticAccessExpr};
use std::sync::Arc;

fn is_valid_class_name_type(ty: &Union) -> bool {
    // Class names must be strings or class-string types
    ty.contains(|t| {
        matches!(
            t,
            Atomic::TString | Atomic::TClassString(_) | Atomic::TLiteralString(_)
        )
    })
}

impl<'a> ExpressionAnalyzer<'a> {
    pub(super) fn analyze_new<'arena, 'src>(
        &mut self,
        n: &NewExpr<'arena, 'src>,
        call_span: php_ast::Span,
        ctx: &mut Context,
    ) -> Union {
        let arg_types: Vec<Union> = n
            .args
            .iter()
            .map(|a| {
                let ty = self.analyze(&a.value, ctx);
                if a.unpack {
                    crate::call::spread_element_type(&ty)
                } else {
                    ty
                }
            })
            .collect();
        let arg_spans: Vec<php_ast::Span> = n.args.iter().map(|a| a.span).collect();
        let arg_names: Vec<Option<String>> = n
            .args
            .iter()
            .map(|a| a.name.as_ref().map(|nm| nm.to_string_repr().into_owned()))
            .collect();
        let arg_can_be_byref: Vec<bool> = n
            .args
            .iter()
            .map(|a| crate::call::expr_can_be_passed_by_reference(&a.value))
            .collect();

        let class_ty = match &n.class.kind {
            ExprKind::Identifier(name) => {
                let resolved = crate::db::resolve_name_via_db(self.db, &self.file, name.as_ref());
                let fqcn: Arc<str> = match resolved.as_str() {
                    "self" | "static" => ctx
                        .self_fqcn
                        .clone()
                        .or_else(|| ctx.static_fqcn.clone())
                        .unwrap_or_else(|| Arc::from(resolved.as_str())),
                    "parent" => ctx
                        .parent_fqcn
                        .clone()
                        .unwrap_or_else(|| Arc::from(resolved.as_str())),
                    _ => Arc::from(resolved.as_str()),
                };
                let type_exists = crate::db::type_exists_via_db(self.db, fqcn.as_ref());
                if !matches!(resolved.as_str(), "self" | "static" | "parent") && !type_exists {
                    self.emit(
                        IssueKind::UndefinedClass {
                            name: resolved.clone(),
                        },
                        Severity::Error,
                        n.class.span,
                    );
                } else if type_exists {
                    if let Some(node) = self
                        .db
                        .lookup_class_node(fqcn.as_ref())
                        .filter(|n| n.active(self.db))
                    {
                        if node.is_abstract(self.db) {
                            self.emit(
                                IssueKind::AbstractInstantiation {
                                    class: fqcn.to_string(),
                                },
                                Severity::Error,
                                n.class.span,
                            );
                        }
                        if let Some(msg) = node.deprecated(self.db) {
                            self.emit(
                                IssueKind::DeprecatedClass {
                                    name: fqcn.to_string(),
                                    message: Some(msg).filter(|m| !m.is_empty()),
                                },
                                Severity::Info,
                                n.class.span,
                            );
                        }
                    }
                    let ctor_params =
                        crate::db::lookup_method_in_chain(self.db, &fqcn, "__construct")
                            .map(|n| n.params(self.db).to_vec());
                    if let Some(ctor_params) = ctor_params {
                        crate::call::check_constructor_args(
                            self,
                            &fqcn,
                            crate::call::CheckArgsParams {
                                fn_name: "__construct",
                                params: &ctor_params,
                                arg_types: &arg_types,
                                arg_spans: &arg_spans,
                                arg_names: &arg_names,
                                arg_can_be_byref: &arg_can_be_byref,
                                call_span,
                                has_spread: n.args.iter().any(|a| a.unpack),
                            },
                        );
                    }
                }
                let ty = Union::single(Atomic::TNamedObject {
                    fqcn: fqcn.clone(),
                    type_params: vec![],
                });
                self.record_symbol(
                    n.class.span,
                    SymbolKind::ClassReference(fqcn.clone()),
                    ty.clone(),
                );
                if !self.inference_only {
                    let (line, col_start, col_end) = self.span_to_ref_loc(n.class.span);
                    self.db.record_reference_location(crate::db::RefLoc {
                        symbol_key: fqcn.clone(),
                        file: self.file.clone(),
                        line,
                        col_start,
                        col_end,
                    });
                }
                ty
            }
            _ => {
                let ty = self.analyze(n.class, ctx);
                // Check if the expression could evaluate to a valid class name
                // (but skip anonymous class definitions, which are valid)
                if !matches!(n.class.kind, ExprKind::AnonymousClass(_))
                    && !is_valid_class_name_type(&ty)
                {
                    self.emit(
                        IssueKind::UndefinedClass {
                            name: "<dynamic>".to_string(),
                        },
                        Severity::Error,
                        n.class.span,
                    );
                }
                // Check abstract for known TClassString
                for atom in ty.types.iter() {
                    if let Atomic::TClassString(Some(fqcn)) = atom {
                        if let Some(node) = self
                            .db
                            .lookup_class_node(fqcn.as_ref())
                            .filter(|n| n.active(self.db))
                        {
                            if node.is_abstract(self.db) {
                                self.emit(
                                    IssueKind::AbstractInstantiation {
                                        class: fqcn.to_string(),
                                    },
                                    Severity::Error,
                                    n.class.span,
                                );
                            }
                        }
                    }
                }
                Union::single(Atomic::TObject)
            }
        };
        class_ty
    }

    pub(super) fn analyze_property_access<'arena, 'src>(
        &mut self,
        pa: &PropertyAccessExpr<'arena, 'src>,
        expr_span: php_ast::Span,
        ctx: &mut Context,
    ) -> Union {
        let obj_ty = self.analyze(pa.object, ctx);
        let prop_name =
            extract_string_from_expr(pa.property).unwrap_or_else(|| "<dynamic>".to_string());

        if obj_ty.contains(|t| matches!(t, Atomic::TNull)) && obj_ty.is_single() {
            self.emit(
                IssueKind::NullPropertyFetch {
                    property: prop_name.clone(),
                },
                Severity::Error,
                expr_span,
            );
            return Union::mixed();
        }
        if obj_ty.is_nullable() {
            self.emit(
                IssueKind::PossiblyNullPropertyFetch {
                    property: prop_name.clone(),
                },
                Severity::Info,
                expr_span,
            );
        }

        if prop_name == "<dynamic>" {
            return Union::mixed();
        }
        let resolved = self.resolve_property_type(&obj_ty, &prop_name, pa.property.span);
        for atomic in &obj_ty.types {
            if let Atomic::TNamedObject { fqcn, .. } = atomic {
                self.record_symbol(
                    pa.property.span,
                    SymbolKind::PropertyAccess {
                        class: fqcn.clone(),
                        property: Arc::from(prop_name.as_str()),
                    },
                    resolved.clone(),
                );
                break;
            }
        }
        resolved
    }

    pub(super) fn analyze_nullsafe_property_access<'arena, 'src>(
        &mut self,
        pa: &PropertyAccessExpr<'arena, 'src>,
        ctx: &mut Context,
    ) -> Union {
        let obj_ty = self.analyze(pa.object, ctx);
        let prop_name =
            extract_string_from_expr(pa.property).unwrap_or_else(|| "<dynamic>".to_string());
        if prop_name == "<dynamic>" {
            return Union::mixed();
        }
        let non_null_ty = obj_ty.remove_null();
        let mut prop_ty = self.resolve_property_type(&non_null_ty, &prop_name, pa.property.span);
        prop_ty.add_type(Atomic::TNull);
        for atomic in &non_null_ty.types {
            if let Atomic::TNamedObject { fqcn, .. } = atomic {
                self.record_symbol(
                    pa.property.span,
                    SymbolKind::PropertyAccess {
                        class: fqcn.clone(),
                        property: Arc::from(prop_name.as_str()),
                    },
                    prop_ty.clone(),
                );
                break;
            }
        }
        prop_ty
    }

    pub(super) fn analyze_static_property_access<'arena, 'src>(
        &mut self,
        spa: &StaticAccessExpr<'arena, 'src>,
    ) -> Union {
        if let ExprKind::Identifier(id) = &spa.class.kind {
            let resolved = crate::db::resolve_name_via_db(self.db, &self.file, id.as_ref());
            if !matches!(resolved.as_str(), "self" | "static" | "parent")
                && !crate::db::type_exists_via_db(self.db, &resolved)
            {
                self.emit(
                    IssueKind::UndefinedClass { name: resolved },
                    Severity::Error,
                    spa.class.span,
                );
            }
        }
        Union::mixed()
    }

    pub(super) fn analyze_class_const_access<'arena, 'src>(
        &mut self,
        cca: &StaticAccessExpr<'arena, 'src>,
        expr_span: php_ast::Span,
    ) -> Union {
        if cca.member.name_str() == Some("class") {
            let fqcn = if let ExprKind::Identifier(id) = &cca.class.kind {
                let resolved = crate::db::resolve_name_via_db(self.db, &self.file, id.as_ref());
                if !matches!(resolved.as_str(), "self" | "static" | "parent") {
                    if !crate::db::type_exists_via_db(self.db, &resolved) {
                        self.emit(
                            IssueKind::UndefinedClass {
                                name: resolved.clone(),
                            },
                            Severity::Error,
                            cca.class.span,
                        );
                    }
                    if !self.inference_only {
                        let (line, col_start, col_end) = self.span_to_ref_loc(cca.class.span);
                        self.db.record_reference_location(crate::db::RefLoc {
                            symbol_key: Arc::from(resolved.as_str()),
                            file: self.file.clone(),
                            line,
                            col_start,
                            col_end,
                        });
                    }
                }
                Some(Arc::from(resolved.as_str()))
            } else {
                None
            };
            return Union::single(Atomic::TClassString(fqcn));
        }

        let const_name = match cca.member.name_str() {
            Some(n) => n.to_string(),
            None => return Union::mixed(),
        };

        let fqcn = match &cca.class.kind {
            ExprKind::Identifier(id) => {
                let resolved = crate::db::resolve_name_via_db(self.db, &self.file, id.as_ref());
                if matches!(resolved.as_str(), "self" | "static" | "parent") {
                    return Union::mixed();
                }
                resolved
            }
            _ => return Union::mixed(),
        };

        if !crate::db::type_exists_via_db(self.db, &fqcn) {
            self.emit(
                IssueKind::UndefinedClass { name: fqcn },
                Severity::Error,
                cca.class.span,
            );
            return Union::mixed();
        }

        if !self.inference_only {
            let (line, col_start, col_end) = self.span_to_ref_loc(cca.class.span);
            self.db.record_reference_location(crate::db::RefLoc {
                symbol_key: Arc::from(fqcn.as_str()),
                file: self.file.clone(),
                line,
                col_start,
                col_end,
            });
        }

        let const_exists = crate::db::class_constant_exists_in_chain(self.db, &fqcn, &const_name);
        if !const_exists && !crate::db::has_unknown_ancestor_via_db(self.db, &fqcn) {
            self.emit(
                IssueKind::UndefinedConstant {
                    name: format!("{fqcn}::{const_name}"),
                },
                Severity::Error,
                expr_span,
            );
        }
        Union::mixed()
    }

    pub(super) fn resolve_property_type(
        &mut self,
        obj_ty: &Union,
        prop_name: &str,
        span: php_ast::Span,
    ) -> Union {
        for atomic in &obj_ty.types {
            match atomic {
                Atomic::TNamedObject { fqcn, .. }
                    if crate::db::class_kind_via_db(self.db, fqcn.as_ref())
                        .is_some_and(|k| !k.is_interface && !k.is_trait && !k.is_enum) =>
                {
                    let prop_found: Option<Union> =
                        crate::db::lookup_property_in_chain(self.db, fqcn.as_ref(), prop_name)
                            .map(|node| node.ty(self.db).unwrap_or_else(Union::mixed));
                    if let Some(ty) = prop_found {
                        if !self.inference_only {
                            let (line, col_start, col_end) = self.span_to_ref_loc(span);
                            self.db.record_reference_location(crate::db::RefLoc {
                                symbol_key: Arc::from(format!("{}::{}", fqcn, prop_name)),
                                file: self.file.clone(),
                                line,
                                col_start,
                                col_end,
                            });
                        }
                        return ty;
                    }
                    if !crate::db::has_unknown_ancestor_via_db(self.db, fqcn.as_ref())
                        && crate::db::lookup_method_in_chain(self.db, fqcn.as_ref(), "__get")
                            .is_none()
                    {
                        self.emit(
                            IssueKind::UndefinedProperty {
                                class: fqcn.to_string(),
                                property: prop_name.to_string(),
                            },
                            Severity::Warning,
                            span,
                        );
                    }
                    return Union::mixed();
                }
                Atomic::TNamedObject { fqcn, .. }
                    if crate::db::class_kind_via_db(self.db, fqcn.as_ref())
                        .is_some_and(|k| k.is_enum) =>
                {
                    match prop_name {
                        "name" => return Union::single(Atomic::TNonEmptyString),
                        "value" => {
                            if let Some(node) = self
                                .db
                                .lookup_class_node(fqcn.as_ref())
                                .filter(|n| n.active(self.db))
                            {
                                if let Some(scalar_ty) = node.enum_scalar_type(self.db) {
                                    return scalar_ty;
                                }
                            }
                            self.emit(
                                IssueKind::UndefinedProperty {
                                    class: fqcn.to_string(),
                                    property: prop_name.to_string(),
                                },
                                Severity::Warning,
                                span,
                            );
                            return Union::mixed();
                        }
                        _ => {
                            self.emit(
                                IssueKind::UndefinedProperty {
                                    class: fqcn.to_string(),
                                    property: prop_name.to_string(),
                                },
                                Severity::Warning,
                                span,
                            );
                            return Union::mixed();
                        }
                    }
                }
                Atomic::TMixed => return Union::mixed(),
                _ => {}
            }
        }
        Union::mixed()
    }
}
