import 'dart:async';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/ansi.dart';
import 'package:path/path.dart' as p;
import 'package:prompts/prompts.dart' as prompts;
import 'package:recase/recase.dart';
import '../random_string.dart' as rs;
import '../util.dart';
import 'key.dart';
import 'rename.dart';

class InitCommand extends Command {
  final KeyCommand _key = KeyCommand();

  @override
  String get name => 'init';

  @override
  String get description =>
      'Initializes a new Angel3 project in the current directory.';

  InitCommand() {
    argParser
      ..addFlag('offline',
          help:
              'Disable online fetching of boilerplates. Also disables `pub-get`.',
          negatable: false)
      ..addFlag('pub-get', defaultsTo: true)
      ..addOption('project-name',
          abbr: 'n', help: 'The name for this project.');
  }

  @override
  void run() async {
    if (argResults == null) {
      print('Invalid arguements');
      return;
    }

    var projectDir =
        Directory(argResults!.rest.isEmpty ? '.' : argResults!.rest[0]);
    print('Creating new Angel3 project in ${projectDir.absolute.path}...');
    await _cloneRepo(projectDir);
    // await preBuild(projectDir);
    var secret = rs.randomAlphaNumeric(32);
    print('Generated new development JWT secret: $secret');
    await _key.changeSecret(
        File.fromUri(projectDir.uri.resolve('config/default.yaml')), secret);

    secret = rs.randomAlphaNumeric(32);
    print('Generated new production JWT secret: $secret');
    await _key.changeSecret(
        File.fromUri(projectDir.uri.resolve('config/production.yaml')), secret);

    var name = argResults!.wasParsed('project-name')
        ? (argResults!['project-name'] as String)
        : p.basenameWithoutExtension(
            projectDir.absolute.uri.normalizePath().toFilePath());

    name = ReCase(name).snakeCase;
    print('Renaming project from "angel" to "$name"...');
    await renamePubspec(projectDir, 'angel', name);
    await renameDartFiles(projectDir, 'angel', name);
    // Renaming executable files

    if (argResults!['pub-get'] != false && argResults!['offline'] == false) {
      print('Now running dart pub get...');
      await _pubGet(projectDir);
    }

    print(green.wrap('$checkmark Successfully initialized Angel3 project.'));

    stdout
      ..writeln()
      ..writeln(
          'Congratulations! You are ready to start developing with Angel3!')
      ..write('To start the server (with ')
      ..write(cyan.wrap('hot-reloading'))
      ..write('), run ')
      ..write(magenta.wrap('`dart --observe bin/dev.dart`'))
      ..writeln(' in your terminal.')
      ..writeln()
      ..writeln('Find more documentation about Angel3:')
      ..writeln('  * https://angel3-framework.web.app')
      ..writeln('  * https://angel3-docs.dukefirehawk.com')
//      ..writeln(
//          '  * https://www.youtube.com/playlist?list=PLl3P3tmiT-frEV50VdH_cIrA2YqIyHkkY')
//      ..writeln('  * https://medium.com/the-angel-framework')
//      ..writeln('  * https://dart.academy/tag/angel')
      ..writeln()
      ..writeln('Happy coding!');
  }

  Future _deleteRecursive(FileSystemEntity entity, [bool self = true]) async {
    if (entity is Directory) {
      await for (var entity in entity.list(recursive: true)) {
        try {
          await _deleteRecursive(entity);
        } catch (e) {
          print(e);
        }
      }

      try {
        if (self != false) await entity.delete(recursive: true);
      } catch (e) {
        print(e);
      }
    } else if (entity is File) {
      try {
        await entity.delete(recursive: true);
      } catch (e) {
        print(e);
      }
    } else if (entity is Link) {
      var path = await entity.resolveSymbolicLinks();
      var stat = await FileStat.stat(path);

      switch (stat.type) {
        case FileSystemEntityType.directory:
          return await _deleteRecursive(Directory(path));
        case FileSystemEntityType.file:
          return await _deleteRecursive(File(path));
        default:
          break;
      }
    }
  }

  Future _cloneRepo(Directory projectDir) async {
    Directory boilerplateDir = Directory("./empty");

    try {
      if (await projectDir.exists()) {
        var shouldDelete = prompts.getBool(
            "Directory '${projectDir.absolute.path}' already exists. Overwrite it?");

        if (!shouldDelete) {
          throw 'Chose not to overwrite existing directory.';
        } else if (projectDir.absolute.uri.normalizePath().toFilePath() !=
            Directory.current.absolute.uri.normalizePath().toFilePath()) {
          await projectDir.delete(recursive: true);
        } else {
          await _deleteRecursive(projectDir, false);
        }
      }

      // var boilerplate = basicBoilerplate;
      print('Choose a project type before continuing:');

      var boilerplate = prompts.choose(
              'Choose a project type before continuing', boilerplates) ??
          basicBoilerplate;

      // Ultimately, we want a clone of every boilerplate locally on the system.
      var boilerplateRootDir = Directory(p.join(angelDir.path, 'boilerplates'));
      var boilerplateBasename = p.basenameWithoutExtension(boilerplate.url);
      if (boilerplate.ref != '') {
        boilerplateBasename += '.${boilerplate.ref}';
      }
      boilerplateDir =
          Directory(p.join(boilerplateRootDir.path, boilerplateBasename));
      await boilerplateRootDir.create(recursive: true);

      var branch = boilerplate.ref;
      if (branch == '') {
        branch = 'master';
      }

      // If there is no clone existing, clone it.
      if (!await boilerplateDir.exists()) {
        if (argResults!['offline'] as bool) {
          throw Exception(
              '--offline was selected, but the "${boilerplate.name}" boilerplate has not yet been downloaded.');
        }

        print(
            'Cloning "${boilerplate.name}" boilerplate from "${boilerplate.url}"...');
        Process git;

        if (boilerplate.ref == '') {
          print(darkGray.wrap(
              '\$ git clone --depth 1 ${boilerplate.url} ${boilerplateDir.absolute.path}'));
          git = await Process.start(
            'git',
            [
              'clone',
              '--depth',
              '1',
              boilerplate.url,
              boilerplateDir.absolute.path
            ],
            mode: ProcessStartMode.inheritStdio,
          );
        } else {
          // git clone --single-branch -b branch host:/dir.git
          print(darkGray.wrap(
              '\$ git clone --depth 1 --single-branch -b ${boilerplate.ref} ${boilerplate.url} ${boilerplateDir.absolute.path}'));
          git = await Process.start(
            'git',
            [
              'clone',
              '--depth',
              '1',
              '--single-branch',
              '-b',
              boilerplate.ref,
              boilerplate.url,
              boilerplateDir.absolute.path
            ],
            mode: ProcessStartMode.inheritStdio,
          );
        }

        if (await git.exitCode != 0) {
          throw Exception('Could not clone repo.');
        }
      }

      // Otherwise, pull from git.
      else if (!(argResults!['offline'] as bool)) {
        print(darkGray.wrap('\$ git pull origin $branch'));
        var git = await Process.start('git', ['pull', 'origin', branch],
            mode: ProcessStartMode.inheritStdio,
            workingDirectory: boilerplateDir.absolute.path);
        if (await git.exitCode != 0) {
          print(yellow.wrap(
              'Update of $branch failed. Attempting to continue with existing contents.'));
        }
      } else {
        print(darkGray.wrap(
            'Using existing contents of "${boilerplate.name}" boilerplate.'));
      }

      // Next, just copy everything into the given directory.
      await copyDirectory(boilerplateDir, projectDir);

      if (boilerplate.needsPrebuild) {
        await preBuild(projectDir).catchError((_) => null);
      }

      var gitDir = Directory.fromUri(projectDir.uri.resolve('.git'));
      if (await gitDir.exists()) await gitDir.delete(recursive: true);
    } catch (e) {
      await boilerplateDir.delete(recursive: true).catchError((e) {
        print('Got error: ${e.error}');
      });

      if (e is! String) {
        print(red.wrap('$ballot Could not initialize Angel3 project.'));
      }
      rethrow;
    }
  }

  Future _pubGet(Directory projectDir) async {
    var dartPath = "dart";
    print(darkGray.wrap('Running "$dartPath"...'));
    print(darkGray.wrap('\$ $dartPath pub get'));
    var dart = await Process.start(dartPath, ['pub', 'get'],
        workingDirectory: projectDir.absolute.path,
        mode: ProcessStartMode.inheritStdio);
    var code = await dart.exitCode;
    print('Dart process exited with code $code');
  }
}

Future preBuild(Directory projectDir) async {
  // Run build
  // print('Running `dart run build_runner build`...');
  print(darkGray.wrap('\$ dart run build_runner build'));

  var build = await Process.start("dart", ['run', 'build_runner', 'build'],
      workingDirectory: projectDir.absolute.path,
      mode: ProcessStartMode.inheritStdio);

  var buildCode = await build.exitCode;

  if (buildCode != 0) throw Exception('Failed to pre-build resources.');
}

const repoLocation = 'https://github.com/dukefirehawk';

const BoilerplateInfo graphQLBoilerplate = BoilerplateInfo(
  'GraphQL',
  'A starter application with GraphQL support.',
  '$repoLocation/boilerplates.git',
  ref: 'angel3-graphql',
);

const BoilerplateInfo ormBoilerplate = BoilerplateInfo(
  'ORM',
  'A starter application with ORM support.',
  '$repoLocation/boilerplates.git',
  ref: 'angel3-orm',
);

const BoilerplateInfo basicBoilerplate = BoilerplateInfo(
    'Basic',
    'A basic starter application with minimal packages.',
    '$repoLocation/boilerplates.git',
    ref: 'angel3-basic');

const BoilerplateInfo sharedBoilerplate = BoilerplateInfo(
    'Shared',
    'Holds common models and files shared across multiple Dart projects.',
    '$repoLocation/boilerplate_shared.git');

const BoilerplateInfo sharedOrmBoilerplate = BoilerplateInfo(
  'Shared (ORM)',
  'Holds common models and files shared across multiple Dart projects.',
  '$repoLocation/boilerplate_shared.git',
  ref: 'orm',
);

const List<BoilerplateInfo> boilerplates = [
  basicBoilerplate,
  ormBoilerplate,
  graphQLBoilerplate,
  //sharedBoilerplate,
  //sharedOrmBoilerplate,
];

class BoilerplateInfo {
  final String name, description, url;
  final String ref;
  final bool needsPrebuild;

  const BoilerplateInfo(this.name, this.description, this.url,
      {this.ref = '', this.needsPrebuild = false});

  @override
  String toString() => '$name ($description)';
}