// 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:io';

import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';

final _pubspecGlob = Glob('**/pubspec.yaml');

/// Searches for all pubspecs under [rootDir] that are a context root (don't
/// have `resolution: workspace`), initializes them, and returns a list of
/// [ContextRoot]s sorted by path.
Future<List<ContextRoot>> getContextRoots(
  String rootDir, {
  bool isSdk = false,
}) async {
  var roots = await _initializeContextRoots(rootDir, isSdk: isSdk).toList();
  _sortContextRoots(roots);
  return roots;
}

/// Runs `dart|flutter pub get` in [projectDir]
///
/// Checks various parts of the [pubspec] for `flutter` dependencies to
/// determine the type of package.
Future<void> runPubGet(Directory projectDir, YamlMap pubspec) async {
  var isFlutter =
      pubspec['environment']?['flutter'] != null ||
      pubspec['dependencies']?['flutter'] != null ||
      pubspec['dev_dependencies']?['flutter'] != null ||
      pubspec['dev_dependencies']?['flutter_test'] != null;
  var sdk = isFlutter
      ? (Platform.isWindows ? 'flutter.bat' : 'flutter')
      : 'dart';
  print('Fetching dependencies with `$sdk pub get` in ${projectDir.path}');
  var pubGetResult = await Process.run(sdk, [
    'pub',
    'get',
    '--no-example',
  ], workingDirectory: projectDir.path);
  if (pubGetResult.exitCode != 0) {
    throw StateError(
      'Failed to run `$sdk pub get`:\n'
      'StdOut:\n${pubGetResult.stdout}\n'
      'StdErr:\n${pubGetResult.stderr}',
    );
  }
}

Future<ContextRoot?> _createContextRoot(
  File pubspecFile, {
  bool isSdk = false,
}) async {
  // Skip hidden dirs.
  if (pubspecFile.uri.pathSegments.any((path) => path.startsWith('.'))) {
    return null;
  }
  try {
    var pubspec = loadYaml(await pubspecFile.readAsString()) as YamlMap;
    if (pubspec['resolution'] == 'workspace') return null;
    var contextRootDir = pubspecFile.parent;
    if (!isSdk) {
      await runPubGet(contextRootDir, pubspec);
    }
    var packageConfig = await findPackageConfig(contextRootDir);
    if (packageConfig == null) {
      throw StateError(
        'Unable to find package config file in ${contextRootDir.path}',
      );
    }
    return ContextRoot(contextRootDir, packageConfig);
  } catch (e) {
    stderr.writeln(
      'Error initializing context root for pubspec at ${pubspecFile.path}:\n'
      '$e',
    );
    return null;
  }
}

Stream<ContextRoot> _initializeContextRoots(
  String rootDir, {
  bool isSdk = false,
}) async* {
  // Explicitly check the root for a pubspec, as the glob sometimes misses it.
  var rootPubspec = File(path.join(rootDir, 'pubspec.yaml'));
  if (await rootPubspec.exists()) {
    var root = await _createContextRoot(rootPubspec, isSdk: isSdk);
    if (root != null) yield root;
  }

  await for (var pubspecFile in _pubspecGlob.list(root: rootDir)) {
    if (pubspecFile is! File) continue;
    // We already processed the root pubspec if it exists, so skip it here if the
    // glob found it too.
    if (pubspecFile.path == rootPubspec.path) continue;

    var root = await _createContextRoot(pubspecFile as File, isSdk: isSdk);
    if (root != null) yield root;
  }
}

/// Sorts the [roots] by their path.
void _sortContextRoots(List<ContextRoot> roots) {
  roots.sort((a, b) => a.dir.path.compareTo(b.dir.path));
}

/// An analysis context root for a project, projects may have many context
/// roots, and they can be subdirectories of each other.
class ContextRoot {
  /// The root directory for this analysis context.
  final Directory dir;

  /// The package config for this context root.
  final PackageConfig packageConfig;

  new(this.dir, this.packageConfig);
}

/// A [ProjectGenerator] represents a reproducible way to create a pristine
/// copy of a development workspace.
///
/// Each call to [setUp] returns a (typically new) instance of a workspace.
abstract interface class ProjectGenerator {
  /// A short description of the project.
  String get description;

  /// Performs any work necessary to initialize the project and returns the
  /// [Workspace] that was created.
  ///
  /// Note that the order these are returned in should be deterministic and
  /// match the original project order.
  Future<Workspace> setUp();

  /// Invoked once the workspace is no longer needed, should perform any
  /// necessary cleanup (delete or restore workspace to original condition).
  Future<void> tearDown(Workspace workspace);
}

/// A generated workspace, roughly corresponds to an IDE workspace, may contain
/// multiple open projects.
class Workspace {
  /// Each context root is a single directory and associated package config.
  ///
  /// These may be subdirectories of the root directories, or other contexts.
  final List<ContextRoot> contextRoots;

  /// The actual root directories containing the projects.
  ///
  /// These are typically just used for cleanup, and defaults to
  /// [workspaceDirectories].
  final Iterable<Directory> rootDirectories;

  /// The open workspace directories.
  ///
  /// These correspond directly to the `workspaceFolder` entries in LSP.
  final Iterable<Directory> workspaceDirectories;

  new({
    required this.contextRoots,
    required this.workspaceDirectories,
    Iterable<Directory>? rootDirectories,
  }) : rootDirectories = rootDirectories ?? workspaceDirectories;
}
