// Copyright 2018 The Chromium Authors. 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:convert';
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:process/process.dart';

class ArchivePublisherException implements Exception {
  ArchivePublisherException(this.message, [this.result]);

  final String message;
  final ProcessResult result;

  @override
  String toString() {
    String output = 'ArchivePublisherException';
    if (message != null) {
      output += ': $message';
    }
    final String stderr = result?.stderr ?? '';
    if (stderr.isNotEmpty) {
      output += ':\n${result.stderr}';
    }
    return output;
  }
}

enum Channel { dev, beta }

/// Publishes the archive created for a particular version and git hash to
/// the releases directory on cloud storage, and updates the metadata for
/// releases.
///
/// See https://github.com/flutter/flutter/wiki/Release-process for more
/// information on the release process.
class ArchivePublisher {
  ArchivePublisher(
    this.revision,
    this.version,
    this.channel, {
    this.processManager = const LocalProcessManager(),
    this.tempDir,
  }) : assert(revision.length == 40, 'Git hash must be 40 characters long (i.e. the entire hash).');

  /// A git hash describing the revision to publish. It should be the complete
  /// hash, not just a prefix.
  final String revision;

  /// A version number for the release (e.g. "1.2.3").
  final String version;

  /// The channel to publish to.
  // TODO(gspencer): support Channel.beta: it is currently unimplemented.
  final Channel channel;

  /// Get the name of the channel as a string.
  String get channelName {
    switch (channel) {
      case Channel.beta:
        return 'beta';
      case Channel.dev:
      default:
        return 'dev';
    }
  }

  /// The process manager to use for invoking commands. Typically only
  /// used for testing purposes.
  final ProcessManager processManager;

  /// The temporary directory used for this publisher. If not set, one will
  /// be created, used, and then removed automatically. If set, it will not be
  /// deleted when done: that is left to the caller. Typically used by tests.
  Directory tempDir;

  static String gsBase = 'gs://flutter_infra';
  static String releaseFolder = '/releases';
  static String baseUrl = 'https://storage.googleapis.com/flutter_infra';
  static String archivePrefix = 'flutter_';
  static String releaseNotesPrefix = 'release_notes_';

  final String metadataGsPath = '$gsBase$releaseFolder/releases.json';

  /// Publishes the archive for the given constructor parameters.
  bool publishArchive() {
    assert(channel == Channel.dev, 'Channel must be dev (beta not yet supported)');
    final List<String> platforms = <String>['linux', 'mac', 'win'];
    final Map<String, String> metadata = <String, String>{};
    for (String platform in platforms) {
      final String src = _builtArchivePath(platform);
      final String dest = _destinationArchivePath(platform);
      final String srcGsPath = '$gsBase$src';
      final String destGsPath = '$gsBase$releaseFolder$dest';
      _cloudCopy(srcGsPath, destGsPath);
      metadata['${platform}_archive'] = '$channelName/$platform$dest';
    }
    metadata['release_date'] = new DateTime.now().toUtc().toIso8601String();
    metadata['version'] = version;
    _updateMetadata(metadata);
    return true;
  }

  /// Checks to make sure the user has access to the Google Storage bucket
  /// required to publish. Will throw an [ArchivePublisherException] if not.
  void checkForGSUtilAccess() {
    // Fetching ACLs requires FULL_CONTROL access.
    final ProcessResult result = _runGsUtil(<String>['acl', 'get', metadataGsPath]);
    if (result.exitCode != 0) {
      throw new ArchivePublisherException(
        'GSUtil cannot get ACLs for metadata file $metadataGsPath',
        result,
      );
    }
  }

  void _updateMetadata(Map<String, String> metadata) {
    final ProcessResult result = _runGsUtil(<String>['cat', metadataGsPath]);
    if (result.exitCode != 0) {
      throw new ArchivePublisherException(
          'Unable to get existing metadata at $metadataGsPath', result);
    }
    final String currentMetadata = result.stdout;
    if (currentMetadata.isEmpty) {
      throw new ArchivePublisherException('Empty metadata received from server', result);
    }
    Map<String, dynamic> jsonData;
    try {
      jsonData = json.decode(currentMetadata);
    } on FormatException catch (e) {
      throw new ArchivePublisherException('Unable to parse JSON metadata received from cloud: $e');
    }
    jsonData['current_$channelName'] = revision;
    if (!jsonData.containsKey('releases')) {
      jsonData['releases'] = <String, dynamic>{};
    }
    if (jsonData['releases'].containsKey(revision)) {
      throw new ArchivePublisherException(
          'Revision $revision already exists in metadata! Aborting.');
    }
    jsonData['releases'][revision] = metadata;
    final Directory localTempDir = tempDir ?? Directory.systemTemp.createTempSync('flutter_');
    final File tempFile = new File(path.join(localTempDir.absolute.path, 'releases.json'));
    final JsonEncoder encoder = const JsonEncoder.withIndent('  ');
    tempFile.writeAsStringSync(encoder.convert(jsonData));
    _cloudCopy(tempFile.absolute.path, metadataGsPath);
    if (tempDir == null) {
      localTempDir.delete(recursive: true);
    }
  }

  String _getArchiveSuffix(String platform) {
    switch (platform) {
      case 'linux':
      case 'mac':
        return '.tar.xz';
      case 'win':
        return '.zip';
      default:
        assert(false, 'platform $platform not recognized.');
        return null;
    }
  }

  String _builtArchivePath(String platform) {
    final String shortRevision = revision.substring(0, revision.length > 10 ? 10 : revision.length);
    final String archivePathBase = '/flutter/$revision/$archivePrefix';
    final String suffix = _getArchiveSuffix(platform);
    return '$archivePathBase${platform}_$shortRevision$suffix';
  }

  String _destinationArchivePath(String platform) {
    final String archivePathBase = '/$channelName/$platform/$archivePrefix';
    final String suffix = _getArchiveSuffix(platform);
    return '$archivePathBase${platform}_$version-$channelName$suffix';
  }

  ProcessResult _runGsUtil(List<String> args) {
    return processManager.runSync(<String>['gsutil']..addAll(args));
  }

  void _cloudCopy(String src, String dest) {
    final ProcessResult result = _runGsUtil(<String>['cp', src, dest]);
    if (result.exitCode != 0) {
      throw new ArchivePublisherException('GSUtil copy command failed: ${result.stderr}', result);
    }
  }
}
