#include <git2.h>
#include <git2/types.h>
#include "git_repo.h"

#include "../templ/duc_git_repo.h"

static orb_rc_t init(orb_module_t *self) {
  duc_git_repo_t *data = self->data;
  git_repo_param_list_t *param_list = self->param_list;
  if (param_list) {
    data->repo = (git_repo_t) {
      .path = param_list->repo_path,
      .meta = {0},
      .handle = NULL,
      .flag_ref_oid = param_list->flag_ref_oid,
      .flag_oid = param_list->flag_oid,
      .flag_path = param_list->flag_path,
      .ref_head = NULL,
      .ref = NULL,
      .ref_list = NULL,
      .ref_list_len = 0,
    };
    data->repo.meta.name = param_list->repo_name;
    data->tab_mode = param_list->tab_mode;
  }
  return ORB_OK;
}

static int _fetch_read_text_file(char *filename, char **content) {
  FILE *fd = fopen(filename, "r");
  if (fd == NULL) return 1;
  fseek(fd, 0, SEEK_END);
  size_t len = ftell(fd);
  fseek(fd, 0, SEEK_SET);
  *content = calloc(1, sizeof(char) * (len + 1));
  fread(*content, sizeof(char), len, fd);
  (*content)[len] = '\0';
  fclose(fd);
  return 0;
}

static int _fetch_read_git_file(char *repo_path, char *filename, char **content) {
  size_t file_path_len = strlen(repo_path) + strlen(filename) + 2;
  char file_path[file_path_len];
  snprintf(file_path, file_path_len, "%s/%s", repo_path, filename);
  return _fetch_read_text_file(file_path, content);
}

static bool _fetch_git_file_exists(char *repo_path, char *filename) {
  size_t file_path_len = strlen(repo_path) + strlen(filename) + 2;
  char file_path[file_path_len];
  snprintf(file_path, file_path_len, "%s/%s", repo_path, filename);
  FILE *fp = fopen(file_path, "r");
  if (!fp) return false;
  fclose(fp);
  return true;
}

static char *_fetch_git_file_oid(git_repository *repo, char *ref_name, char *filename) {
  size_t spec_len = strlen(ref_name) + strlen(filename) + 2;
  char spec[spec_len];
  snprintf(spec, spec_len, "%s:%s", ref_name, filename);
  git_object *obj_handle = NULL;
  if (git_revparse_single(&obj_handle, repo, spec) != 0) return NULL;
  char oid_str[GIT_OID_HEXSZ + 1];
  git_oid_tostr(oid_str, sizeof(oid_str), git_object_id(obj_handle));
  git_object_free(obj_handle);
  return strdup(oid_str);
}

static orb_rc_t fetch(orb_module_t *self) {
  duc_git_repo_t *data = self->data;
  git_repo_param_list_t *param_list = self->param_list;
  if (!param_list->repo_path) return ORB_FATAL(1);
  bool is_public = _fetch_git_file_exists(param_list->repo_path, "git-daemon-export-ok");
  if (!is_public) return ORB_FATAL(2);
  git_repository *repo_handle = NULL;
  int code = git_repository_open(&repo_handle, param_list->repo_path);
  if (code < 0 || repo_handle == NULL) return ORB_FATAL(code);
  data->repo.handle = repo_handle;
  _fetch_read_git_file(data->repo.path, "description", &data->repo.meta.description);
  _fetch_read_git_file(data->repo.path, "owner", &data->repo.meta.owner);
  data->repo.meta.is_public = is_public;
  data->repo.meta.is_archive = _fetch_git_file_exists(data->repo.path, ".archive");
  data->repo.meta.is_fork = _fetch_git_file_exists(data->repo.path, ".fork");
  git_reference *ref_head_handle = NULL;
  git_repository_head(&ref_head_handle, data->repo.handle);
  if (ref_head_handle) {
    data->repo.ref_head = calloc(1, sizeof(git_repo_ref_t));
    git_repo_ref_t *ref = data->repo.ref_head;
    const git_oid *oid = git_reference_target(ref_head_handle);
    char oid_str[GIT_OID_HEXSZ + 1];
    git_oid_tostr(oid_str, sizeof(oid_str), oid);
    ref->oid = strdup(oid_str);
    const char *ref_name = git_reference_name(ref_head_handle);
    if (git_reference_is_branch(ref_head_handle)) {
      ref->name = strdup(ref_name + strlen("refs/heads/"));
      ref->type = "branch";
    }
    if (git_reference_is_tag(ref_head_handle)) {
      ref->name = strdup(ref_name + strlen("refs/tags/"));
      ref->type = "tag";
    }
    if (git_reference_is_remote(ref_head_handle)) {
      ref->name = strdup(ref_name + strlen("refs/remotes/"));
      ref->type = "remote";
    }
    if (!ref->name) {
      ref->name = strdup(ref_name);
    }
    git_reference_free(ref_head_handle);
    data->repo.readme_oid = _fetch_git_file_oid(repo_handle, ref->name, "README.md");
    data->repo.license_oid = _fetch_git_file_oid(repo_handle, ref->name, "LICENSE");
    git_reference_iterator *iter = NULL;
    git_reference *ref_handle = NULL;
    git_reference_iterator_new(&iter, data->repo.handle);
    while (git_reference_next(&ref_handle, iter) == 0) {
      data->repo.ref_list = realloc(data->repo.ref_list, sizeof(git_repo_ref_t) * (data->repo.ref_list_len + 1));
      git_repo_ref_t *ref = &data->repo.ref_list[data->repo.ref_list_len];
      memset(ref, 0, sizeof(git_repo_ref_t));
      const git_oid *oid = git_reference_target(ref_handle);
      char oid_str[GIT_OID_HEXSZ + 1];
      git_oid_tostr(oid_str, sizeof(oid_str), oid);
      ref->oid = strdup(oid_str);
      const char *ref_name = git_reference_name(ref_handle);
      if (git_reference_is_branch(ref_handle)) {
        ref->name = strdup(ref_name + strlen("refs/heads/"));
        ref->type = "branch";
      }
      if (git_reference_is_tag(ref_handle)) {
        ref->name = strdup(ref_name + strlen("refs/tags/"));
        ref->type = "tag";
      }
      if (git_reference_is_remote(ref_handle)) {
        ref->name = strdup(ref_name + strlen("refs/remotes/"));
        ref->type = "remote";
      }
      if (!ref->name) {
        ref->name = strdup(ref_name);
      }
      git_reference_free(ref_handle);
      data->repo.ref_list_len++;
    }
    git_reference_iterator_free(iter);
  }
  if (data->repo.ref_head && data->repo.flag_ref_oid && strcmp(data->repo.ref_head->oid, data->repo.flag_ref_oid) != 0) {
    git_oid ref_oid;
    if (git_oid_fromstr(&ref_oid, data->repo.flag_ref_oid) == 0) {
      git_reference_iterator *iter = NULL;
      git_reference *ref_handle = NULL;
      git_reference_iterator_new(&iter, data->repo.handle);
      while (git_reference_next(&ref_handle, iter) == 0) {
        const git_oid *oid = git_reference_target(ref_handle);
        if (oid && git_oid_equal(oid, &ref_oid)) {
          data->repo.ref = calloc(1, sizeof(git_repo_ref_t));
          git_repo_ref_t *ref = data->repo.ref;
          ref->oid = strdup(data->repo.flag_ref_oid);
          const char *ref_name = git_reference_name(ref_handle);
          if (git_reference_is_branch(ref_handle)) {
            ref->name = strdup(ref_name + strlen("refs/heads/"));
            ref->type = "branch";
          }
          if (git_reference_is_tag(ref_handle)) {
            ref->name = strdup(ref_name + strlen("refs/tags/"));
            ref->type = "tag";
          }
          if (git_reference_is_remote(ref_handle)) {
            ref->name = strdup(ref_name + strlen("refs/remotes/"));
            ref->type = "remote";
          }
          if (!ref->name) {
            ref->name = strdup(ref_name);
          }
        }
        git_reference_free(ref_handle);
      }
      git_reference_iterator_free(iter);
      if (!data->repo.ref) {
        git_object *obj_handle = NULL;
        if (git_object_lookup(&obj_handle, data->repo.handle, &ref_oid, GIT_OBJECT_COMMIT) == 0) {
          git_object_free(obj_handle);
          data->repo.ref_list = realloc(data->repo.ref_list, sizeof(git_repo_ref_t) * (data->repo.ref_list_len + 1));
          git_repo_ref_t *ref = &data->repo.ref_list[data->repo.ref_list_len];
          memset(ref, 0, sizeof(git_repo_ref_t));
          ref->oid = strdup(data->repo.flag_ref_oid);
          ref->name = strdup(data->repo.flag_ref_oid);
          ref->type = "commit";
          data->repo.ref_list_len++;
          data->repo.ref = calloc(1, sizeof(git_repo_ref_t));
          ref = data->repo.ref;
          ref->oid = strdup(data->repo.flag_ref_oid);
          ref->name = strdup(data->repo.flag_ref_oid);
          ref->type = "commit";
        }
      }
    }
  }
  return ORB_OK;
}

static orb_rc_t render(orb_module_t *self, void *out) {
  return ORB_ERR(duc_git_repo_render(out, (duc_git_repo_t *) self->data));
}

static void cleanup(orb_module_t *self) {
  duc_git_repo_t *data = self->data;
  if (data->repo.handle) {
    git_repository_free(data->repo.handle);
    data->repo.handle = NULL;
  }
  if (data->repo.meta.description) free(data->repo.meta.description);
  if (data->repo.meta.owner) free(data->repo.meta.owner);
  if (data->repo.ref_head) {
    free(data->repo.ref_head->oid);
    free(data->repo.ref_head->name);
    free(data->repo.ref_head);
    data->repo.ref_head = NULL;
  }
  if (data->repo.ref) {
    free(data->repo.ref->oid);
    free(data->repo.ref->name);
    free(data->repo.ref);
    data->repo.ref = NULL;
  }
  if (data->repo.readme_oid) free(data->repo.readme_oid);
  if (data->repo.license_oid) free(data->repo.license_oid);
  if (data->repo.ref_list && data->repo.ref_list_len > 0) {
    for (size_t i = 0; i < data->repo.ref_list_len; i++) {
      git_repo_ref_t *ref = &data->repo.ref_list[i];
      free(ref->oid);
      free(ref->name);
    }
    free(data->repo.ref_list);
    data->repo.ref_list = NULL;
    data->repo.ref_list_len = 0;
  }
}

const orb_module_type_t GIT_REPO_TYPE = {
  .size = sizeof(duc_git_repo_t),
  .init = init,
  .fetch = fetch,
  .render = render,
  .cleanup = cleanup,
};
