use rustc_hash::FxHashSet;

use oxc_allocator::{ArenaVec, ReplaceWith, TakeIn};
use oxc_ast::{ast::*, builder::NONE};
use oxc_semantic::{ScopeFlags, ScopeId};
use oxc_span::SPAN;
use oxc_str::Ident;
use oxc_syntax::operator::AssignmentOperator;
use oxc_traverse::BoundIdentifier;

use crate::{
    common::computed_key::{create_computed_key_temp_var, key_needs_temp_var},
    context::TraverseCtx,
    utils::ast_builder::{
        create_class_constructor, create_this_property_access, create_this_property_assignment,
    },
};

use super::TypeScript;

impl<'a> TypeScript<'a> {
    /// Transform class fields, and constructor parameters that includes modifiers into `this` assignments.
    ///
    /// This transformation is doing 2 things:
    ///
    /// 1. Convert constructor parameters that include modifier to `this` assignments and insert them
    ///    after the super call in the constructor body.
    ///
    /// Same as `Self::convert_constructor_params` does, the reason why we still need that method because
    /// this method only runs when `set_public_class_fields` is `true` and the `class_properties` plugin
    /// is disabled. The other branches (`class_properties` enabled, or `set_public_class_fields: false`
    /// — see [`Self::add_parameter_property_fields`]) reach the parameter-property assignments via
    /// `convert_constructor_params` directly. Merging them together would add unnecessary checks for
    /// the param-only paths.
    ///
    /// 2. Convert class fields to `this` assignments in the constructor body.
    ///
    /// > This transformation only works when `set_public_class_fields` is `true`,
    /// > and the fields have initializers, which is to align with the behavior of TypeScript's
    /// > `useDefineForClassFields: false` option.
    ///
    /// Input:
    /// ```ts
    /// class C {
    ///   x = 1;
    ///   [y] = 2;
    /// }
    /// ```
    ///
    /// Output:
    /// ```js
    /// let _y;
    /// class C {
    ///   static {
    ///     _y = y;
    ///   }
    ///   constructor() {
    ///     this.x = 1;
    ///     this[_y] = 2;
    ///   }
    /// }
    /// ```
    ///
    /// The computed key transformation behavior is the same as `TypeScript`.
    /// Computed key assignments are inserted into a static block, unlike Babel which inserts them before class.
    /// We follow `TypeScript` just for simplicity, because `Babel` handles class expressions and class declarations
    /// differently, which is quite troublesome to implement.
    /// Anyway, `TypeScript` is the source of truth for the typescript transformation.
    ///
    /// For static properties, we convert them to static blocks.
    ///
    /// Input:
    /// ```ts
    /// class C {
    ///   static x = 1;
    ///   static [y] = 2;
    /// }
    /// ```
    ///
    /// Output:
    /// ```js
    /// let _y;
    /// class C {
    ///   static {
    ///     this.x = 1;
    ///   }
    ///   static {
    ///     this[_y] = 2;
    ///   }
    /// }
    /// ```
    ///
    /// The transformation way is also the same as `TypeScript`, the advantage from the implementation is that
    /// we don't need extra transformation for static properties, the output is the same as instance properties
    /// transformation, and the greatest advantage is we don't need to care about `this` usage in static block.
    pub(super) fn transform_class_fields(&self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) {
        // When any non-private instance field has an initializer, all instance field
        // initializers (including private) must be hoisted to the constructor to preserve
        // execution order. This matches TypeScript's `WillHoistInitializersToConstructor`:
        // https://github.com/microsoft/TypeScript/blob/7b8cb3bdf8/src/compiler/transformers/classFields.ts#L339
        let will_hoist_initializers_to_constructor = class.body.body.iter().any(|e| {
            matches!(e, ClassElement::PropertyDefinition(prop)
                if !prop.r#static && !prop.declare && !prop.key.is_private_identifier() && prop.value.is_some()
            )
        });

        // Static blocks created below must be parented to the class body scope,
        // not the current traversal scope (which is the scope enclosing the class).
        let class_scope_id = class.scope_id();

        let mut constructor = None;
        let mut property_assignments = Vec::new();
        let mut computed_key_assignments = Vec::new();
        for element in &mut class.body.body {
            match element {
                // Skip static private properties — converting them to static blocks
                // would lose the private field declaration.
                ClassElement::PropertyDefinition(prop)
                    if !prop.key.is_private_identifier()
                        || (!prop.r#static && will_hoist_initializers_to_constructor) =>
                {
                    if let Some(value) = prop.value.take() {
                        let assignment = self.convert_property_definition(
                            &mut prop.key,
                            value,
                            &mut computed_key_assignments,
                            ctx,
                        );
                        if prop.r#static {
                            // Convert static property to static block
                            // `class C { static x = 1; }` -> `class C { static { this.x = 1; } }`
                            // `class C { static [x] = 1; }` -> `let _x; class C { static { this[_x] = 1; } }`
                            let body = ArenaVec::from_value_in(assignment, ctx);
                            *element = Self::create_class_static_block(body, class_scope_id, ctx);
                        } else {
                            property_assignments.push(assignment);
                        }
                    } else if self.remove_class_fields_without_initializer
                        && let Some(key) = prop.key.as_expression_mut()
                    {
                        // `TypeScript` uses `isSimpleInlineableExpression` to check if the key needs to be kept.
                        // There is a little difference that we treat `BigIntLiteral` and `RegExpLiteral` can be kept, and
                        // `IdentifierReference` without symbol is not kept.
                        // https://github.com/microsoft/TypeScript/blob/8c62e08448e0ec76203bd519dd39608dbcb31705/src/compiler/transformers/classFields.ts#L2720
                        if key_needs_temp_var(key, ctx) {
                            // When `remove_class_fields_without_initializer` is true, the property without initializer
                            // would be removed in the `transform_class_on_exit`. We need to make sure the computed key
                            // keeps and is evaluated in the same order as the original class field in static block.
                            computed_key_assignments.push(key.take_in(ctx));
                        }
                    }
                }
                ClassElement::MethodDefinition(method) => {
                    if method.kind == MethodDefinitionKind::Constructor {
                        constructor = Some(&mut method.value);
                    } else {
                        Self::convert_computed_key(
                            &mut method.key,
                            &mut computed_key_assignments,
                            ctx,
                        );
                    }
                }
                ClassElement::AccessorProperty(accessor) => {
                    Self::convert_computed_key(
                        &mut accessor.key,
                        &mut computed_key_assignments,
                        ctx,
                    );
                }
                _ => (),
            }
        }

        let computed_key_assignment_static_block =
            (!computed_key_assignments.is_empty()).then(|| {
                let sequence_expression = Expression::new_sequence_expression(
                    SPAN,
                    ArenaVec::from_iter_in(computed_key_assignments, ctx),
                    ctx,
                );
                let statement = Statement::new_expression_statement(SPAN, sequence_expression, ctx);
                Self::create_class_static_block(
                    ArenaVec::from_value_in(statement, ctx),
                    class_scope_id,
                    ctx,
                )
            });

        if let Some(constructor) = constructor {
            let params = &constructor.params.items;

            let params_assignment = Self::convert_constructor_params(params, ctx);
            property_assignments.splice(0..0, params_assignment);

            if property_assignments.is_empty() {
                // No property/parameter assignments to inject, but computed-key temp
                // inits still need their static block (e.g. `static [expr] = value` +
                // empty `constructor()`), otherwise the temp var is left uninitialized.
                if let Some(element) = computed_key_assignment_static_block {
                    class.body.body.insert(0, element);
                }
                return;
            }

            // `constructor {}` is guaranteed that it is `Some`.
            let constructor_body_statements = &mut constructor.body.as_mut().unwrap().statements;
            let super_call_position = Self::get_super_call_position(constructor_body_statements);

            // Insert the assignments after the `super()` call
            constructor_body_statements
                .splice(super_call_position..super_call_position, property_assignments);

            // Insert the static block after the constructor if there is a constructor
            if let Some(element) = computed_key_assignment_static_block {
                class.body.body.insert(0, element);
            }
        } else if !property_assignments.is_empty() {
            // If there is no constructor, we need to create a default constructor
            // that initializes the public fields
            // TODO: should use `ctx.insert_scope_below_statements`, but it only accept an `ArenaVec` rather than std `Vec`.
            let scope_id = ctx.create_child_scope(
                class_scope_id,
                ScopeFlags::StrictMode | ScopeFlags::Function | ScopeFlags::Constructor,
            );
            let ctor = create_class_constructor(
                property_assignments,
                class.super_class.is_some(),
                scope_id,
                ctx,
            );

            // Insert the static block at the beginning of the class body if there is no constructor
            if let Some(element) = computed_key_assignment_static_block {
                class.body.body.splice(0..0, [ctor, element]);
            } else {
                // TODO(improve-on-babel): Could push constructor onto end of elements, instead of inserting as first
                class.body.body.insert(0, ctor);
            }
        } else if let Some(element) = computed_key_assignment_static_block {
            class.body.body.insert(0, element);
        }
    }

    /// Insert an uninitialized class field declaration for each constructor parameter property.
    ///
    /// Aligns with TypeScript's output under `useDefineForClassFields: true`, which is the
    /// default since TypeScript 4.0. Runs when [`crate::CompilerAssumptions::set_public_class_fields`]
    /// is `false` (the inverse of the `useDefineForClassFields: false` mapping).
    ///
    /// Input:
    /// ```ts
    /// class C {
    ///   constructor(public x = 1) {}
    /// }
    /// ```
    ///
    /// Output:
    /// ```js
    /// class C {
    ///   x;
    ///   constructor(x = 1) {
    ///     this.x = x;
    ///   }
    /// }
    /// ```
    ///
    /// Skips parameter properties whose name matches an existing instance field declaration on
    /// the same class (matching TypeScript's dedup). Only static-identifier instance fields
    /// count — static, private, computed, and `declare`-only fields don't suppress the new field.
    pub(super) fn add_parameter_property_fields(class: &mut Class<'a>, ctx: &TraverseCtx<'a>) {
        let Some(params) = Self::find_constructor_params(class) else { return };

        // Bail before allocating when there are no parameter properties to emit.
        // Parameter properties are TS-specific and uncommon, so most classes hit this path.
        if !params.iter().any(FormalParameter::has_modifier) {
            return;
        }

        let mut declared_names = Self::collect_instance_field_names(class);
        let fields = ArenaVec::from_iter_in(
            params
                .iter()
                .filter(|param| param.has_modifier())
                .filter_map(|param| param.pattern.get_binding_identifier())
                .filter(|id| declared_names.insert(id.name))
                .map(|id| Self::build_uninitialized_field(id, ctx)),
            ctx,
        );

        class.body.body.splice(0..0, fields);
    }

    /// Returns the body-bearing constructor's parameter list, or `None` if the class has none.
    fn find_constructor_params<'b>(
        class: &'b Class<'a>,
    ) -> Option<&'b ArenaVec<'a, FormalParameter<'a>>> {
        class.body.body.iter().find_map(|element| {
            if let ClassElement::MethodDefinition(method) = element
                && method.kind.is_constructor()
                && method.value.body.is_some()
            {
                Some(&method.value.params.items)
            } else {
                None
            }
        })
    }

    /// Collect names of instance field declarations keyed by a static identifier.
    ///
    /// Static, private, computed, and `declare`-only fields don't suppress a parameter-property
    /// field — they're different bindings or erased before emit. Plain instance-field collisions
    /// are a TS `Duplicate identifier` error, so this dedup is mostly defensive.
    fn collect_instance_field_names(class: &Class<'a>) -> FxHashSet<Ident<'a>> {
        class
            .body
            .body
            .iter()
            .filter_map(|element| match element {
                ClassElement::PropertyDefinition(prop)
                    if !prop.r#static
                        && !prop.declare
                        && let PropertyKey::StaticIdentifier(ident) = &prop.key =>
                {
                    Some(ident.name)
                }
                ClassElement::AccessorProperty(prop)
                    if !prop.r#static
                        && let PropertyKey::StaticIdentifier(ident) = &prop.key =>
                {
                    Some(ident.name)
                }
                _ => None,
            })
            .collect()
    }

    /// Build `<id.name>;` — an uninitialized public class field declaration.
    ///
    /// Safe to read `id.name` directly here — `add_parameter_property_fields` is gated on
    /// `!is_class_properties_plugin_enabled`, so no rename has happened.
    /// `convert_constructor_params` uses the span trick because it also runs alongside
    /// class-properties.
    fn build_uninitialized_field(
        id: &BindingIdentifier<'a>,
        ctx: &TraverseCtx<'a>,
    ) -> ClassElement<'a> {
        let key = PropertyKey::new_static_identifier(id.span, id.name, ctx);
        ClassElement::new_property_definition(
            id.span,
            PropertyDefinitionType::PropertyDefinition,
            ArenaVec::new_in(ctx),
            key,
            NONE,
            None,
            false,
            false,
            false,
            false,
            false,
            false,
            false,
            None,
            ctx,
        )
    }

    pub(super) fn transform_class_on_exit(&self, class: &mut Class<'a>, ctx: &TraverseCtx<'a>) {
        if !self.remove_class_fields_without_initializer {
            return;
        }

        class.body.body.retain(|element| {
            if let ClassElement::PropertyDefinition(prop) = element
                && prop.value.is_none()
                && !prop.key.is_private_identifier()
            {
                if let Some(key) = prop.key.as_expression() {
                    return key_needs_temp_var(key, ctx);
                }
                return false;
            }
            true
        });
    }

    /// Transform constructor parameters that include modifier to `this` assignments and
    /// insert them after the super call in the constructor body.
    ///
    /// Input:
    /// ```ts
    /// class C {
    ///   constructor(public x, private y) {}
    /// }
    /// ```
    ///
    /// Output:
    /// ```js
    /// class C {
    ///  constructor(x, y) {
    ///   this.x = x;
    ///   this.y = y;
    /// }
    /// ```
    pub(super) fn transform_class_constructor(
        constructor: &mut MethodDefinition<'a>,
        ctx: &mut TraverseCtx<'a>,
    ) {
        if !constructor.kind.is_constructor() || constructor.value.body.is_none() {
            return;
        }

        let params = &constructor.value.params.items;
        let assignments = Self::convert_constructor_params(params, ctx).collect::<Vec<_>>();

        let constructor_body_statements = &mut constructor.value.body.as_mut().unwrap().statements;
        let super_call_position = Self::get_super_call_position(constructor_body_statements);

        // Insert the assignments after the `super()` call
        constructor_body_statements.splice(super_call_position..super_call_position, assignments);
    }

    /// Convert property definition to `this` assignment in constructor.
    ///
    /// * Computed key:
    ///   `class C { [x()] = 1; }` -> `let _x; class C { static { _x = x(); } constructor() { this[_x] = 1; } }`
    /// * Static key:
    ///   `class C { x = 1; }` -> `class C { constructor() { this.x = 1; } }`
    ///
    /// Returns an assignment statement which would be inserted in the constructor body.
    #[expect(clippy::unused_self)]
    fn convert_property_definition(
        &self,
        key: &mut PropertyKey<'a>,
        value: Expression<'a>,
        computed_key_assignments: &mut Vec<Expression<'a>>,
        ctx: &mut TraverseCtx<'a>,
    ) -> Statement<'a> {
        let member = match key {
            PropertyKey::StaticIdentifier(ident) => {
                create_this_property_access(SPAN, ident.name, ctx)
            }
            PropertyKey::PrivateIdentifier(ident) => {
                // Accessor backing fields: `this.#prop = value`
                let ident = PrivateIdentifier::new(SPAN, ident.name, ctx);
                MemberExpression::new_private_field_expression(
                    SPAN,
                    Expression::new_this_expression(SPAN, ctx),
                    ident,
                    false,
                    ctx,
                )
            }
            key @ match_expression!(PropertyKey) => {
                let key = key.to_expression_mut();
                // Note: Key can also be static `StringLiteral` or `NumericLiteral`.
                // `class C { 'x' = true; 123 = false; }`
                // No temp var is created for these.
                let new_key = if key_needs_temp_var(key, ctx) {
                    let taken_key = key.take_in(ctx);
                    let (assignment, ident) = create_computed_key_temp_var(taken_key, ctx);
                    computed_key_assignments.push(assignment);
                    ident
                } else {
                    key.take_in(ctx)
                };

                MemberExpression::new_computed_member_expression(
                    SPAN,
                    Expression::new_this_expression(SPAN, ctx),
                    new_key,
                    false,
                    ctx,
                )
            }
        };
        let target = AssignmentTarget::from(member);
        Self::create_assignment(target, value, ctx)
    }

    /// Find the position of the `super()` call in the constructor body, otherwise return 0.
    ///
    /// Don't need to handle nested `super()` call because `TypeScript` doesn't allow it.
    pub fn get_super_call_position(statements: &[Statement<'a>]) -> usize {
        // Find the position of the `super()` call in the constructor body.
        // Don't need to handle nested `super()` call because `TypeScript` doesn't allow it.
        statements
            .iter()
            .position(|stmt| {
                matches!(stmt, Statement::ExpressionStatement(stmt)
                        if stmt.expression.is_super_call_expression())
            })
            .map_or(0, |pos| pos + 1)
    }

    /// Convert computed key to sequence expression if there are assignments.
    ///
    /// Input:
    /// ```ts
    /// class C {
    ///   [x()] = 1;
    ///   [y()]() {}
    ///   [x()] = 2;
    /// }
    /// ```
    ///
    /// Output:
    /// ```js
    /// let _x, _x2;
    /// class C {
    ///   constructor() {
    ///     this[_x] = 1;
    ///     this[_x2] = 2;
    ///   }
    ///   static {
    ///     _x2 = x();
    ///   }
    ///   [(_x = x(), y())]() {}
    /// }
    /// ```
    ///
    /// So that computed key keeps running in the same order as the original class field.
    #[inline]
    fn convert_computed_key(
        key: &mut PropertyKey<'a>,
        assignments: &mut Vec<Expression<'a>>,
        ctx: &TraverseCtx<'a>,
    ) {
        if assignments.is_empty() {
            return;
        }
        if let Some(key) = key.as_expression_mut() {
            // If the key is already an expression, we need to create a new expression sequence
            // to insert the assignments into.
            key.replace_with(|original_key| {
                Expression::new_sequence_expression(
                    SPAN,
                    ArenaVec::from_iter_in(
                        assignments.split_off(0).into_iter().chain(std::iter::once(original_key)),
                        ctx,
                    ),
                    ctx,
                )
            });
        }
    }

    /// Convert constructor parameters that include modifier to `this` assignments
    pub(super) fn convert_constructor_params(
        params: &ArenaVec<'a, FormalParameter<'a>>,
        ctx: &mut TraverseCtx<'a>,
    ) -> impl Iterator<Item = Statement<'a>> {
        let source_text = ctx.state.source_text;
        params
            .iter()
            .filter(|param| param.has_modifier())
            .filter_map(|param| param.pattern.get_binding_identifier())
            .map(|id| {
                // `id.name` may be renamed by clash detection; use span to recover the original source name.
                let prop_name = Ident::from(id.span.source_text(source_text));
                let target = create_this_property_assignment(id.span, prop_name, ctx);
                let value = BoundIdentifier::from_binding_ident(id).create_read_expression(ctx);
                Self::create_assignment(target, value, ctx)
            })
    }

    /// Create `a.b = value`
    fn create_assignment(
        target: AssignmentTarget<'a>,
        value: Expression<'a>,
        ctx: &TraverseCtx<'a>,
    ) -> Statement<'a> {
        Statement::new_expression_statement(
            SPAN,
            Expression::new_assignment_expression(
                SPAN,
                AssignmentOperator::Assign,
                target,
                value,
                ctx,
            ),
            ctx,
        )
    }

    /// Create `static { body }` as a child of `class_scope_id`.
    ///
    /// The enclosing traversal scope is the scope around the class, not the class body,
    /// so pass `class.scope_id()` explicitly to parent the static block correctly.
    #[inline]
    fn create_class_static_block(
        body: ArenaVec<'a, Statement<'a>>,
        class_scope_id: ScopeId,
        ctx: &mut TraverseCtx<'a>,
    ) -> ClassElement<'a> {
        let scope_id = ctx.insert_scope_below_statement_from_scope_id(
            &body[0],
            class_scope_id,
            ScopeFlags::StrictMode | ScopeFlags::ClassStaticBlock,
        );

        ClassElement::new_static_block_with_scope_id(SPAN, body, scope_id, ctx)
    }
}
