import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';

/// custom_lint entry point — discovered by the plugin loader.
PluginBase createPlugin() => _HaloLintsPlugin();

class _HaloLintsPlugin extends PluginBase {
  @override
  List<LintRule> getLintRules(CustomLintConfigs configs) => [
        const NoRefInDisposeRule(),
      ];
}

/// Flags any use of `ref` (read/watch/listen/invalidate/etc.) inside the
/// `dispose()` method of a class extending `ConsumerState` /
/// `ConsumerStatefulWidgetState`.
///
/// Why: Riverpod invalidates `ref` the instant `State.dispose()` enters. The
/// resulting `Bad state: Cannot use "ref" after the widget was disposed.` is
/// caught silently inside `BuildOwner.finalizeTree`, leaves the widget tree
/// half-finalized, and the next-pushed screen appears frozen. Real instances
/// caught in this repo (2026-05-14): `home_screen.dart`, `payment_screen.dart`.
///
/// Fix: move the ref-using cleanup into `deactivate()`, which runs BEFORE
/// `dispose()` while `ref` is still valid. See client_app/CLAUDE.md → Pitfalls.
class NoRefInDisposeRule extends DartLintRule {
  const NoRefInDisposeRule() : super(code: _code);

  static const _code = LintCode(
    name: 'no_ref_in_dispose',
    problemMessage:
        "Don't use 'ref' in dispose(). Riverpod invalidates ref the moment "
        "dispose() runs; the resulting error is swallowed and silently "
        "corrupts the widget tree (next screen freezes). Move this cleanup "
        "to deactivate() instead. See client_app/CLAUDE.md → Pitfalls.",
  );

  @override
  void run(
    CustomLintResolver resolver,
    ErrorReporter reporter,
    CustomLintContext context,
  ) {
    context.registry.addMethodDeclaration((method) {
      if (method.name.lexeme != 'dispose') return;
      final cls = method.parent;
      if (cls is! ClassDeclaration) return;

      // Walk up the supertype chain by name — element resolution is more
      // robust but text-match is enough for the two real superclasses we
      // care about, and avoids a full type resolve on every method.
      final superName = cls.extendsClause?.superclass.name2.lexeme;
      if (superName == null) return;
      if (!superName.startsWith('Consumer')) return;

      method.body.accept(_RefUsageVisitor(reporter));
    });
  }
}

class _RefUsageVisitor extends GeneralizingAstVisitor<void> {
  final ErrorReporter reporter;
  _RefUsageVisitor(this.reporter);

  @override
  void visitSimpleIdentifier(SimpleIdentifier node) {
    // Match the bare identifier `ref` whenever it's USED (not declared).
    // Declarations like `final ref = ...` are skipped via inDeclarationContext.
    if (node.name == 'ref' && !node.inDeclarationContext()) {
      reporter.atNode(node, NoRefInDisposeRule._code);
    }
    super.visitSimpleIdentifier(node);
  }
}
