// Copyright (c) 2018, 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:convert';
import 'dart:io';

import 'package:build/build.dart';
// ignore: implementation_imports
import 'package:build_runner_core/src/asset_graph/graph.dart';
// ignore: implementation_imports
import 'package:build_runner_core/src/asset_graph/node.dart';
import 'package:glob/glob.dart';
import 'package:shelf/shelf.dart' as shelf;

import 'path_to_asset_id.dart';

/// A handler for `/$graph` requests under a specific `rootDir`.
class AssetGraphHandler {
  final AssetReader _reader;
  final String _rootPackage;
  final AssetGraph _assetGraph;

  AssetGraphHandler(this._reader, this._rootPackage, this._assetGraph);

  /// Returns a response with the information about a specific node in the
  /// graph.
  ///
  /// For an empty path, returns the HTML page to render the graph.
  ///
  /// For queries with `q=QUERY` will look for the assetNode referenced.
  /// QUERY can be an [AssetId] or a path.
  ///
  /// [AssetId] as `package|path`
  ///
  /// path as:
  /// - `packages/<package>/<path_under_lib>`
  /// - `<path_under_root_package>`
  /// - `<path_under_rootDir>`
  ///
  /// There may be some ambiguity between paths which are under the top-level of
  /// the root package, and those which are under the rootDir. Preference is
  /// given to the asset (if it exists) which is not under the implicit
  /// `rootDir`. For instance if the request is `$graph/web/main.dart` this will
  /// prefer to serve `<package>|web/main.dart`, but if it does not exist will
  /// fall back to `<package>|web/web/main.dart`.
  FutureOr<shelf.Response> handle(shelf.Request request, String rootDir) async {
    switch (request.url.path) {
      case '':
        if (!request.url.hasQuery) {
          return shelf.Response.ok(
              await _reader.readAsString(
                  AssetId('build_runner', 'lib/src/server/graph_viz.html')),
              headers: {HttpHeaders.contentTypeHeader: 'text/html'});
        }

        var query = request.url.queryParameters['q']?.trim();
        if (query != null && query.isNotEmpty) {
          var filter = request.url.queryParameters['f']?.trim();
          return _handleQuery(query, rootDir, filter: filter);
        }
        break;
      case 'assets.json':
        return _jsonResponse(_assetGraph.serialize());
    }

    return shelf.Response.notFound('Bad request: "${request.url}".');
  }

  Future<shelf.Response> _handleQuery(String query, String rootDir,
      {String? filter}) async {
    var filterGlob = filter != null ? Glob(filter) : null;
    var pipeIndex = query.indexOf('|');

    AssetId assetId;
    if (pipeIndex < 0) {
      var querySplit = query.split('/');

      assetId = pathToAssetId(
          _rootPackage, querySplit.first, querySplit.skip(1).toList());

      if (!_assetGraph.contains(assetId)) {
        var secondTry = pathToAssetId(_rootPackage, rootDir, querySplit);

        if (!_assetGraph.contains(secondTry)) {
          return shelf.Response.notFound(
              'Could not find asset for path "$query". Tried:\n'
              '- $assetId\n'
              '- $secondTry');
        }
        assetId = secondTry;
      }
    } else {
      assetId = AssetId.parse(query);
      if (!_assetGraph.contains(assetId)) {
        return shelf.Response.notFound(
            'Could not find asset in build graph: $assetId');
      }
    }
    var node = _assetGraph.get(assetId)!;
    var currentEdge = 0;
    var nodes = [
      {'id': '${node.id}', 'label': '${node.id}'}
    ];
    var edges = <Map<String, String>>[];
    for (final output in node.outputs) {
      if (filterGlob != null && !filterGlob.matches(output.toString())) {
        continue;
      }
      edges.add(
          {'from': '${node.id}', 'to': '$output', 'id': 'e${currentEdge++}'});
      nodes.add({'id': '$output', 'label': '$output'});
    }
    if (node is NodeWithInputs) {
      for (final input in node.inputs) {
        if (filterGlob != null && !filterGlob.matches(input.toString())) {
          continue;
        }
        edges.add(
            {'from': '$input', 'to': '${node.id}', 'id': 'e${currentEdge++}'});
        nodes.add({'id': '$input', 'label': '$input'});
      }
    }
    var result = <String, dynamic>{
      'primary': {
        'id': '${node.id}',
        'hidden': node is GeneratedAssetNode ? node.isHidden : null,
        'state': node is NodeWithInputs ? '${node.state}' : null,
        'wasOutput': node is GeneratedAssetNode ? node.wasOutput : null,
        'isFailure': node is GeneratedAssetNode ? node.isFailure : null,
        'phaseNumber': node is NodeWithInputs ? node.phaseNumber : null,
        'type': node.runtimeType.toString(),
        'glob': node is GlobAssetNode ? node.glob.pattern : null,
        'lastKnownDigest': node.lastKnownDigest.toString(),
      },
      'edges': edges,
      'nodes': nodes,
    };
    return _jsonResponse(_jsonUtf8Encoder.convert(result));
  }
}

final _jsonUtf8Encoder = JsonUtf8Encoder();

shelf.Response _jsonResponse(List<int> body) => shelf.Response.ok(body,
    headers: {HttpHeaders.contentTypeHeader: 'application/json'});
