import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:preferences/preferences.dart';
import 'package:localstorage/localstorage.dart';
import 'package:yaml/yaml.dart';
import 'package:toolheim/data/warband_roster.dart';

class GitHubAdapter extends ChangeNotifier {
  final LocalStorage storage = new LocalStorage('rosters');

  List<String> _syncErrors = new List<String>();
  DateTime _lastSync;

  List<WarbandRoster> _rosters = [];
  String _activeRosterFilePath;

  String get repository => PrefService.getString('repository');
  String get path => PrefService.getString('path');

  bool _syncInProgress = false;
  bool get isSyncInProgress => _syncInProgress;

  DateTime get lastSync => _lastSync;
  List<String> get syncErrors => _syncErrors;

  List<WarbandRoster> get rosters => _rosters;

  WarbandRoster get activeRoster {
    if (_rosters.length == 0) {
      return null;
    }

    if (_activeRosterFilePath == null) {
      return _rosters.first;
    }
    return _rosters.firstWhere(
        (roster) => roster.filePath == _activeRosterFilePath,
        orElse: () => _rosters.first);
  }

  set activeRoster(WarbandRoster roster) {
    _activeRosterFilePath = roster.filePath;
    notifyListeners();
  }

  GitHubAdapter() {
    load();
  }

  /// Search for warband files in the GitHub repository
  ///
  /// This method will search for matching files and check their content in a
  /// subfolder (see fields [repository] and [path]). If the file
  /// contain errors or can't read, a sync error message will be written into
  /// the [syncErrors] list.
  Future<void> search() async {
    Stream<String> warbandFileStream() async* {
      // Get all files which could be potential warband files (end with
      // mordheim.yml and contain the word "heros").

      // TODO: Check rate limit
      // TODO: Extract github access to separate class
      http.Response response = await http.get(
          "https://api.github.com/search/code?q=heros+repo:" +
              repository +
              "+filename:mordheim.yml+path:\"" +
              path +
              "\"");

      // GitHub is not reachable
      if (response.statusCode != 200) {
        _syncErrors.add('Could not find any warband roster files.');
        yield null;
        return;
      }

      // No valid response from GitHub
      dynamic searchResults;
      try {
        searchResults = jsonDecode(response.body);
      } on FormatException catch (e) {
        _syncErrors.add('Could not parse GitHub response. ' + e.toString());
        yield null;
        return;
      }

      // Find suitable files for examination
      RegExp fileRegex = new RegExp(r"[a-zA-Z]+\.mordheim\.ya?ml");
      for (dynamic searchResult in searchResults['items']) {
        if (fileRegex.hasMatch(searchResult['name'])) {
          yield searchResult['path'].toString();
        }
      }
    }

    _syncErrors.clear();
    _rosters.clear();
    _syncInProgress = true;

    notifyListeners();

    await for (String filePath in warbandFileStream()) {
      WarbandRoster roster = await fetchWarband(filePath);
      Version latestVersion = await getLatestVersion(filePath);

      if (roster != null && latestVersion != null) {
        roster.playerName = getPlayerNameFromFilePath(filePath);
        roster.version = latestVersion;
        roster.filePath = filePath;

        _rosters.add(roster);
        notifyListeners();
      }
    }

    // Sort by CP
    _rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints));

    _lastSync = DateTime.now();
    _syncInProgress = false;

    await save();
    notifyListeners();
  }

  Future<void> update() async {
    _syncInProgress = true;
    _syncErrors.clear();

    notifyListeners();

    for (var i = 0; i < rosters.length; i++) {
      Version newVersion = await getLatestVersion(rosters[i].filePath);

      // File does not exist any more, we remove the roster
      if (newVersion == null) {
        rosters.removeAt(i);
        notifyListeners();
        continue;
      }

      // New version found, so we fetch the updated roster
      if (newVersion.gitHash != rosters[i].version.gitHash) {
        WarbandRoster newRoster = await fetchWarband(rosters[i].filePath);

        if (newRoster == null) {
          continue;
        }

        newRoster.playerName = rosters[i].playerName;
        newRoster.version = newVersion;
        newRoster.filePath = rosters[i].filePath;
        rosters[i] = newRoster;
        notifyListeners();
      }
    }

    _rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints));

    _lastSync = DateTime.now();
    _syncInProgress = false;

    await save();
    notifyListeners();
  }

  String getPlayerNameFromFilePath(String filePath) {
    // We try to get the name of the player from the name of the folder
    // in which the file resists
    String cleanedPath = path;
    if (path.startsWith('/')) {
      cleanedPath = cleanedPath.substring(1);
    }

    if (cleanedPath.endsWith('/')) {
      cleanedPath = cleanedPath.substring(0, cleanedPath.length - 1);
    }
    List<String> pathParts =
        filePath.substring(cleanedPath.length + 1).split('/');

    String playerName = 'Lonely Recluse';
    if (pathParts.length >= 2) {
      playerName = pathParts.first;
    }

    return playerName;
  }

  Future<Version> getLatestVersion(String filePath) async {
    // Fetch last change and some metainformation of the file
    http.Response response = await http.get("https://api.github.com/repos/" +
        repository +
        "/commits?path=" +
        filePath);

    if (response.statusCode != 200) {
      _syncErrors
          .add(filePath + ': Could not load the warband metadata from GitHub.');
      notifyListeners();
      return null;
    }

    // No valid response from GitHub
    dynamic commits;
    try {
      commits = jsonDecode(response.body);
    } on FormatException catch (e) {
      _syncErrors
          .add(filePath + ': Could not parse GitHub response. ' + e.toString());
      notifyListeners();
      return null;
    }

    // No commits available
    if (commits.length == 0) {
      return null;
    }

    dynamic latestCommit = commits.first;
    return new Version(
        latestCommit['sha'],
        latestCommit['commit']['committer']['date'],
        latestCommit['commit']['author']['name'],
        latestCommit['commit']['message']);
  }

  Future<WarbandRoster> fetchWarband(String filePath) async {
    http.Response response;
    try {
      response = await http.get("https://raw.githubusercontent.com/" +
          repository +
          '/master/' +
          filePath);
    } catch (e) {
      // We ignore this error, because it will handle from the _syncErrors
      // later (see below).
    }

    if (response == null) {
      return null;
    }

    try {
      YamlMap yamlObject = loadYaml(response.body);
      return WarbandRoster.fromJson(yamlObject);
    } catch (e) {
      _syncErrors.add(filePath + ': ' + e.message);
      notifyListeners();
    }

    return null;
  }

  Future<void> save() async {
    await storage.clear();
    await storage.setItem('lastSync', _lastSync.toIso8601String());
    await storage.setItem('rosters', _rosters);
    await storage.setItem('activeRosterFilePath', _activeRosterFilePath);
  }

  load() {
    storage.ready.then((_) {
      String lastSync = storage.getItem('lastSync');
      if (lastSync != null) {
        _lastSync = DateTime.parse(lastSync);
      }

      _activeRosterFilePath = storage.getItem('activeRosterFilePath');

      List<dynamic> rosters = storage.getItem('rosters');
      if (rosters != null) {
        rosters.forEach((warband) {
          _rosters.add(WarbandRoster.fromJson(warband));
        });
      }
      notifyListeners();
    });
  }
}
