// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io' as io;

import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/source/error_processor.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';

import 'models.dart';

/// Helper to collect diagnostics (hook point for timing/metrics if needed).
Future<List<HarnessDiagnostic>> collectAllDiagnostics(
  ABEngine engine,
  List<String> files,
) async {
  return await engine.collect(files);
}

/// Thin wrapper around AnalysisContextCollection with policy flags.
class ABEngine {
  final String label;
  final OverlayResourceProvider overlay;
  final List<String> roots;

  /// If `true`, a new [AnalysisContextCollectionImpl] is created before any
  /// analysis, without using any caching.
  ///
  /// Not compatible with [withFineDependencies].
  final bool rebuildEveryStep;

  /// If `true`, [AnalysisContextCollectionImpl] is created with the
  /// corresponding flag.
  final bool withFineDependencies;

  AnalysisContextCollectionImpl? _collection;

  ABEngine({
    required this.label,
    required this.overlay,
    required this.roots,
    required this.rebuildEveryStep,
    required this.withFineDependencies,
  });

  AnalysisContextCollection get collection {
    return _collection ??= AnalysisContextCollectionImpl(
      includedPaths: roots,
      resourceProvider: overlay,
      withFineDependencies: withFineDependencies,
    );
  }

  /// Collect diagnostics for [files].
  ///
  /// If [rebuildEveryStep] is true, instantiates a fresh collection to
  /// force conservative recomputation.
  Future<List<HarnessDiagnostic>> collect(List<String> files) async {
    if (rebuildEveryStep) {
      _collection = null;
    }

    var out = <HarnessDiagnostic>[];
    for (var file in files) {
      var analysisContext = collection.contextFor(file);
      var analysisSession = analysisContext.currentSession;
      var errorsResult = await analysisSession.getErrors(file);
      if (errorsResult is ErrorsResult) {
        for (var diagnostic in errorsResult.diagnostics) {
          // Filter TODOs, not interesting for comparison.
          if (diagnostic.diagnosticCode.type == DiagnosticType.TODO) {
            continue;
          }
          var severityName = _getProcessedSeverity(errorsResult, diagnostic);
          if (severityName == null) {
            continue;
          }
          out.add(
            HarnessDiagnostic(
              path: file,
              code: diagnostic.diagnosticCode.lowerCaseName,
              severity: severityName,
              offset: diagnostic.offset,
              length: diagnostic.length,
              message: diagnostic.message,
            ),
          );
        }
      }
    }
    return out;
  }

  /// Notify that [path] has changed (overlay updated).
  Future<void> notifyChange(String path) async {
    if (_collection case var collection?) {
      var analysisContext = collection.contextFor(path);
      analysisContext.changeFile(path);
      await analysisContext.applyPendingFileChanges();
    }
  }

  void resetPerformance() {
    _collection?.scheduler.accumulatedPerformance = OperationPerformanceImpl(
      '<scheduler>',
    );
  }

  void writePerformanceTo(String path) {
    if (_collection case var collection?) {
      var scheduler = collection.scheduler;
      var buffer = StringBuffer();
      scheduler.accumulatedPerformance.write(buffer: buffer);
      scheduler.accumulatedPerformance = OperationPerformanceImpl(
        '<scheduler>',
      );
      io.File(path).writeAsStringSync(buffer.toString());
    }
  }

  /// Returns the name of the severity of the [diagnostic], after applying
  /// processors from the [errorsResult], or `null` if the diagnostic should
  /// be filtered.
  String? _getProcessedSeverity(
    ErrorsResult errorsResult,
    Diagnostic diagnostic,
  ) {
    var processor = ErrorProcessor.getProcessor(
      errorsResult.analysisOptions,
      diagnostic,
    );

    var severityName = diagnostic.diagnosticCode.severity.name;
    if (processor != null) {
      var severity = processor.severity;
      if (severity == null || severity.name == 'NONE') {
        return null;
      }
      severityName = severity.name;
    }

    return severityName;
  }
}

/// Separate selector context used only for site discovery / validation.
/// We provide resolved units for semantic kinds (e.g., alpha) without warming A/B.
class SiteSelector {
  final OverlayResourceProvider overlay;
  final List<String> roots;
  late AnalysisContextCollection _collection;

  SiteSelector(this.overlay, this.roots) {
    _collection = AnalysisContextCollection(
      includedPaths: roots,
      resourceProvider: overlay,
    );
  }

  /// Notify that [path] has changed (overlay updated).
  Future<void> notifyChange(String path) async {
    var analysisContext = _collection.contextFor(path);
    analysisContext.changeFile(path);
    await analysisContext.applyPendingFileChanges();
  }

  CompilationUnit parsedUnit(String path) {
    var analysisContext = _collection.contextFor(path);
    var analysisSession = analysisContext.currentSession;
    var result = analysisSession.getParsedUnit(path);
    result as ParsedUnitResult;
    return result.unit;
  }

  Future<CompilationUnit?> resolvedUnit(String path) async {
    var analysisContext = _collection.contextFor(path);
    var analysisSession = analysisContext.currentSession;
    var result = await analysisSession.getResolvedUnit(path);
    return result.tryCast<ResolvedUnitResult>()?.unit;
  }
}
