import 'dart:async';
import 'dart:io';
// ignore: depend_on_referenced_packages
import 'package:ftpconnect/ftpconnect.dart';
// ignore: implementation_imports
import 'package:dart_framework/utils/path.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command_runtime.dart';

class FTP {
  final String _host;
  final int _port;
  final String _user;
  final String _pass;
  final CommandRuntime _runtime;

  String? _currentRemoteDir;
  FTPConnect? _ftp;

  FTP(String host,
      {int port = 21,
      String user = 'anonymous',
      String password = '',
      int timeout = 30,
      CommandRuntime runtime = const DefaultCommandRuntime()})
      : _host = host,
        _port = port,
        _user = user,
        _pass = password,
        _runtime = runtime {
    _ftp = FTPConnect(host,
        port: port, user: user, pass: password, timeout: timeout);
  }

  Future<void> connect() async {
    int tryCount = 0;
    bool connected = false;
    while (!connected && tryCount < 10) {
      try {
        await _ftp!.connect();
        connected = true;
      } on Exception catch (e) {
        tryCount++;
        var code = e.hashCode;
        var type = e.runtimeType;
        Application.log('========================= Connect Failed!');
        Application.log('$code = $type');
        if (tryCount < 10) {
          await Future.delayed(Duration(seconds: 1));
        }
      }
    }
    if (!connected) {
      throw Exception('FTP connect failed after $tryCount attempts: $_host');
    }
  }

  Future<void> disconnect() async {
    await _ftp!.disconnect();
  }

  //Remote target must be folder
  Future<void> upload(Map<String, String> localRemoteMap) async {
    var cache = <String, Map<String, FileType>>{};
    Map<String, FileType> map = {};
    _currentRemoteDir = null;
    try {
      var entries = localRemoteMap.entries;
      for (var entry in entries) {
        var local = Path.getNormalizePath(entry.key);
        var remote = Path.getNormalizePath(entry.value);
        var parent = Path.getParent(remote);
        if (_currentRemoteDir != parent) {
          await _ftp!.changeDirectory(parent!);
          _currentRemoteDir = parent;
          if (cache.containsKey(_currentRemoteDir)) {
            map = cache[_currentRemoteDir]!;
          } else {
            map = await list(parent);
            cache[_currentRemoteDir!] = map;
          }
        }
        if (map.containsKey(remote) && map[remote] == FileType.directory) {
          await _upload(local, remote);
        }
      }
    } finally {}
  }

  Future<void> _upload(String local, String remote) async {
    if (await Directory(local).exists()) {
      var name = Path.getName(local);
      var childRemoteFolder = '$remote/$name';
      await createFolder(childRemoteFolder);
      var list = Directory(local).listSync();
      for (var item in list) {
        if (await Directory(item.path).exists()) {
          await _upload(item.path, childRemoteFolder);
        } else {
          await _ftp!.changeDirectory(childRemoteFolder);
          await _ftp!.uploadFile(File(item.path));
        }
      }
    } else {
      await _ftp!.changeDirectory(remote);
      await _ftp!.uploadFile(File(local));
    }
  }

  Map<String, Map<String, FileType>> _cache = <String, Map<String, FileType>>{};
  //Local target must be folder
  Future<void> download(Map<String, String> remoteLocalMap) async {
    var c = Completer();
    _cache = <String, Map<String, FileType>>{};
    try {
      var entries = remoteLocalMap.entries;
      for (var entry in entries) {
        await _download(entry.key, entry.value);
      }
    } finally {
      c.complete();
    }
    return c.future;
  }

  Future<void> _download(String remote, String local) async {
    Map<String, FileType>? map;
    remote = Path.getNormalizePath(remote);
    local = Path.getNormalizePath(local);
    var parent = Path.getParent(remote);
    var name = Path.getName(remote);
    if (_cache.containsKey(parent)) {
      map = _cache[parent]!;
    } else {
      map ??= await list(parent!);
    }
    if (map.containsKey(remote)) {
      if (map[remote] == FileType.directory) {
        var childLocalFolder = '$local/$name';
        var dir = Directory(childLocalFolder);
        if (!await dir.exists()) dir.createSync();
        Map<String, FileType> childList;
        if (_cache.containsKey(remote)) {
          childList = _cache[remote]!;
        } else {
          childList = await list(remote);
          _cache[remote] = childList;
        }
        var entries = childList.entries;
        for (var entry in entries) {
          await _download(entry.key, childLocalFolder);
        }
      } else {
        await _ftp!.changeDirectory(parent!);
        await _ftp!.downloadFile(name, File('$local/$name'));
      }
    }
  }

  Future<Map<String, FileType>> list(String path) async {
    var c = Completer<Map<String, FileType>>();
    var map = <String, FileType>{};
    try {
      await _ftp!.changeDirectory(path);
      var list = await _ftp!.listDirectoryContent();
      for (var item in list) {
        FileType? type;
        switch (item.type) {
          case FTPEntryType.dir:
            type = FileType.directory;
            break;
          case FTPEntryType.file:
            type = FileType.file;
            break;
          case FTPEntryType.link:
          case FTPEntryType.unknown:
            break;
        }
        var name = item.name;
        if (name != '.' && name != '..') {
          var remotePath = '$path/$name';
          map[remotePath] = type!;
//          Application.log('$count = $type = $remotePath');
        }
      }
    } finally {}
    c.complete(map);
    return c.future;
  }

  Future<void> delete(List<String> remoteList) async {
    var cache = <String, Map<String, FileType>>{};
    Map<String, FileType>? map;
    _currentRemoteDir = null;
    try {
      for (var item in remoteList) {
        var parent = Path.getParent(item);
        var name = Path.getName(item);
        if (_currentRemoteDir != parent) {
          await _ftp!.changeDirectory(parent!);
          _currentRemoteDir = parent;
          if (cache.containsKey(_currentRemoteDir)) {
            map = cache[_currentRemoteDir!]!;
          } else {
            map = await list(parent);
            cache[_currentRemoteDir!] = map;
          }
        }
        if (map!.containsKey(item)) {
          if (map[item] == FileType.directory) {
            await _ftp!.deleteDirectory(name);
          } else {
            await _ftp!.deleteFile(name);
          }
          Application.log('Deleted: $item');
        }
      }
    } finally {}
  }

  Future<bool> createFolder(String path) async {
    var c = Completer<bool>();
    try {
      var parent = Path.getParent(path);
      var name = Path.getName(path);
      await _ftp!.changeDirectory(parent!);
      if (await _ftp!.checkFolderExistence(name)) {
        c.complete(false);
        Application.log('Create failed: Already exist file or folder');
      } else {
        await _ftp!.makeDirectory(name);
        c.complete(true);
        Application.log('Created Folder: $path');
      }
    } finally {}
    return c.future;
  }
}

// ignore: slash_for_doc_comments
/********************************* SFTP **********************************
Need sshpass

//Brew install - https://brew.sh/index_ko
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

//sshpass install - https://gist.github.com/arunoda/7790979
brew install https://raw.githubusercontent.com/kadwanev/bigboybrew/master/Library/Formula/sshpass.rb

*************************************************************************/

class SFTP extends FTP {
  SFTP(super.host,
      {super.port = 22,
      super.user,
      super.password,
      super.timeout,
      super.runtime});

  @override
  Future<void> connect() async {}

  @override
  Future<void> disconnect() async {}

  Future<ProcessResult> _runSftpCommand(List<String> args,
      {bool? printStdout, bool? printStderr}) async {
    final result = await _runtime.runExecutable('sshpass', args,
        printStdout: printStdout, printStderr: printStderr);
    if (result.exitCode != 0) {
      throw Exception(
          'sshpass failed (exit ${result.exitCode}): ${result.stderr}');
    }
    return result;
  }

  //Remote target must be folder
  @override
  Future<void> upload(Map<String, String> localRemoteMap) async {
    Map<String, FileType>? map;
    var entries = localRemoteMap.entries;
    for (var entry in entries) {
      var local = Path.getNormalizePath(entry.key);
      var remote = Path.getNormalizePath(entry.value);
      String type;
      var parent = Path.getParent(remote);
      if (map == null || !map.containsKey(local)) map = await list(parent!);
      if (map[remote] == FileType.directory) {
        if (await Directory(local).exists()) {
          type = '[D]';
          await _runSftpCommand([
            '-p',
            _pass,
            'scp',
            '-P',
            '$_port',
            '-r',
            local,
            '$_user@$_host:$remote'
          ]);
        } else {
          type = '[F]';
          await _runSftpCommand([
            '-p',
            _pass,
            'scp',
            '-P',
            '$_port',
            local,
            '$_user@$_host:$remote'
          ]);
        }
        Application.log('Uploaded: $type$local --> $remote');
      } else {
        throw ArgumentError('Remote target should be "Folder"');
      }
    }
  }

  //Local target must be folder
  @override
  Future<void> download(Map<String, String> remoteLocalMap) async {
    Map<String, FileType>? map;
    var entries = remoteLocalMap.entries;
    for (var entry in entries) {
      var remote = Path.getNormalizePath(entry.key);
      var local = Path.getNormalizePath(entry.value);
      String type;
      var parent = remote.substring(0, remote.lastIndexOf('/'));
      if (map == null || !map.containsKey(remote)) map = await list(parent);
      if (map[remote] == FileType.directory) {
        type = '[D]';
        await _runSftpCommand([
          '-p',
          _pass,
          'scp',
          '-P',
          '$_port',
          '-r',
          '$_user@$_host:$remote',
          local
        ]);
      } else {
        type = '[F]';
        await _runSftpCommand([
          '-p',
          _pass,
          'scp',
          '-P',
          '$_port',
          '$_user@$_host:$remote',
          local
        ]);
      }
      Application.log('Download: $type$remote --> $local');
    }
  }

  @override
  Future<Map<String, FileType>> list(String path) async {
    var c = Completer<Map<String, FileType>>();
    var map = <String, FileType>{};
    var count = 0;
    final result = await _runSftpCommand(
        ['-p', _pass, 'ssh', '$_user@$_host', '-p$_port', 'ls -l $path'],
        printStdout: false);
    var data = result.stdout.toString();
    var list = data.split('\n');
    for (var element in list) {
      if (count != 0 && element.length > 10) {
        var info = element.split(' ');
        var typeStr = info[0].substring(0, 1);
        FileType type;
        var fileName = info[info.length - 1];
        switch (typeStr) {
          case 'd':
            type = FileType.directory;
            break;
          case '-':
            type = FileType.file;
            break;
          default:
            type = FileType.link;
        }
        var mapKey = '$path/$fileName';
        map[mapKey] = type;
      }
      count++;
    }
    c.complete(map);
    return c.future;
  }

  @override
  Future<void> delete(List<String> remoteList) async {
    Application.log('######################## delete ${remoteList.length}');
    for (var item in remoteList) {
      await _runSftpCommand(
          ['-p', _pass, 'ssh', '$_user@$_host', '-p$_port', 'rm -rf $item']);
      Application.log('Deleted: $item');
    }
  }

  @override
  Future<bool> createFolder(String path) async {
    var c = Completer<bool>();
    var parent = path.substring(0, path.lastIndexOf('/'));
    var map = await list(parent);
    if (map.containsKey(path)) {
      c.complete(false);
      Application.log('Create failed: Already exist file or folder');
    } else {
      await _runSftpCommand(
          ['-p', _pass, 'ssh', '$_user@$_host', '-p$_port', 'mkdir $path']);
      c.complete(true);
      Application.log('Created Folder: $path');
    }
    return c.future;
  }
}
