package utils;
use strict;
use warnings;
use utf8;
use Carp qw(croak);

use LWP::UserAgent;
use JSON;

sub json_api {
    my $url = shift;
    
    my $ua = LWP::UserAgent->new;
    my $json = JSON->new->utf8->indent;
    
    my $res = $ua->get(
        "https://api.github.com$url"
    );
    
    $res->is_success or croak $res->status_line;
    
    return $json->decode($res->content);
}

sub get {
    my $url = shift;
    my %parameters = @_;
    
    my $parameters = '';
    while (my($key, $value) = each %parameters) {
        $parameters .= "&$key=$value";
    }
    
    my $page = 1;
    my $data = [];
    while(1) {
        my $result = json_api("$url?per_page=100&page=$page$parameters");
        if (ref($result) eq 'ARRAY' && scalar @$result > 0) {
            push @$data, @$result;
            $page++;
            
            next;
        }
        last;
    }
    
    return $data;
}

package GitHubBackup;

use strict;
use warnings;
use utf8;
use Carp qw(croak);
use File::Spec;


# both hash and hashref are acceptable
sub new {
    my $class = shift;
    my $args = (ref $_[0] eq 'HASH') ? $_[0] : {@_};
    
    return bless $args, $class;
}

sub account {
    my $self = shift;
    my $args = shift;
    
    if (defined $args) {
        $self->{repos} = undef;
        $self->{account} = $args;
    }
    
    return $self->{account};
}

sub repository {
    my $self = shift;
    my $args = shift;
    
    if (defined $args) {
        $self->{repos} = undef;
        $self->{repository} = $args;
    }
    
    return $self->{repository};
}

sub directory {
    my $self = shift;
    my $args = shift;
    
    if (defined $args) {
        $self->{directory} = File::Spec->rel2abs($args);
    }
    
    return $self->{directory};
}

sub repos {
    my $self = shift;
    return $self->{repos} if ($self->{repos});
    
    $self->{repos} = [];
    
    my $account = $self->account or croak "account is not set";
    my $result;
    if (my $repository = $self->repository) {
        $result = [ utils::json_api("/repos/$account/$repository") ];
    }
    else {
        $result = utils::get("/users/$account/repos");
    }
    
    foreach my $repos (@$result) {
        push @{$self->{repos}},
            GitHubBackup::Repository->new({
                directory => sub {$self->directory},
                repos     => $repos,
            })
        ;
    }
    
    return $self->{repos};
}

sub backup {
    my $self = shift;
    
    foreach my $repos (@{$self->repos}) {
        $repos->backup;
    }
    
    return $self;
}


package GitHubBackup::Repository;

use strict;
use warnings;
use utf8;
use Carp qw(croak);
use Git::Repository;
use File::chdir;
use File::Spec;
use File::Path qw(mkpath);
use LWP::UserAgent;
use JSON;


sub new {
    my $class = shift;
    my $args  = shift;
    
    return bless $args, $class;
}

sub clone_url {
    return (shift)->{repos}{clone_url};
}

sub full_name {
    return (shift)->{repos}{full_name};
}

sub has_downloads {
    return (shift)->{repos}{has_downloads};
}

sub forks_count {
    return (shift)->{repos}{forks_count};
}

sub has_wiki {
    return (shift)->{repos}{has_wiki};
}

sub has_issues {
    return (shift)->{repos}{has_issues};
}

sub directory {
    my $self = shift;
    
    my $path = $self->full_name;
    if (my $base = $self->{directory}->()) {
        $path = File::Spec->catfile($base, $path);
    }
    
    return $path;
}

sub message {
    my $self = shift;
    my $message  = shift;
    
    print $self->full_name, " $message\n";
    
    return $self;
}

sub sync {
    my $self = shift;
    my $url = shift;
    my $dir = shift;
    
    if (-d "$dir") {
        local $CWD = $dir;
        $self->message("++> $dir");
        Git::Repository->run(fetch => '--all');
        return $self;
    }
    
    $self->message("==> $dir");
    mkpath $dir;
    Git::Repository->run(clone => '--mirror' => $url => $dir);
    
    return $self;
}

sub clone_git {
    my $self = shift;
    
    my $dir = $self->directory . '.git';
    my $url = $self->clone_url;
    
    $self->sync($url => $dir);
    
    return $self;
}

sub forks {
    my $self = shift;
    return $self->{forks} if ($self->{forks});
    
    $self->{forks} = utils::get("/repos/" . $self->full_name . "/forks");
    
    return $self->{forks};
}

sub set_forks {
    my $self = shift;
    
    my $dir = $self->directory . '.git';
    local $CWD = $dir;
    
    my $remotes = Git::Repository->run(branch => '--remotes');
    my @fetch;
    foreach my $fork (@{$self->forks}) {
        if ($remotes =~ /$fork->{full_name}/) {
            $self->message("--- ". $fork->{full_name});
            next;
        }
        $self->message("+++ ". $fork->{full_name});
        Git::Repository->run(remote => add => $fork->{full_name} => $fork->{clone_url});
        push @fetch, $fork->{full_name};
    }
    
    foreach my $fork (@fetch) {
        $self->message("--> $fork");
        Git::Repository->run(fetch => $fork);
    }
    
    return $self;
}

sub clone_wiki {
    my $self = shift;
    
    my $dir = $self->directory . '.wiki.git';
    my $url = 'https://github.com/' . $self->full_name . '.wiki.git';
    
    my $ua = LWP::UserAgent->new(max_redirect => 0);
    my $res = $ua->head(
        'https://github.com/' . $self->full_name . '/wiki'
    );
    if ($res->code != 200) {
        $self->message("wiki does not exist");
        return $self;
    }
    
    $self->sync($url => $dir);
    
    return $self;
}

sub issues {
    my $self = shift;
    return $self->{issues} if ($self->{issues});
    
    my $open   = utils::get("/repos/" . $self->full_name . "/issues");
    my $closed = utils::get("/repos/" . $self->full_name . "/issues", state => 'closed');
    
    if ($open)   { push @{$self->{issues}}, @$open }
    if ($closed) { push @{$self->{issues}}, @$closed }
    
    return $self->{issues};
}

sub save_issues {
    my $self = shift;
    
    my $ua = LWP::UserAgent->new;
    my $json = JSON->new->utf8->indent;
    
    my $dir = $self->directory . '.issues';
    mkpath $dir unless (-d $dir);
    local $CWD = $dir;
    foreach my $issue (@{$self->issues}) {
        my $number = $issue->{number};
        $self->message("+++ issue/$number");
        
        open my $fh, ">$number.json";
        print $fh $json->encode($issue);
        close $fh;
        
        if (exists $issue->{pull_request}{patch_url}) {
            $ua->mirror($issue->{pull_request}{patch_url} => "$number.patch");
        }
    }
    
    return $self;
}

sub backup {
    my $self = shift;
    
    $self->clone_git   if ($self->has_downloads eq 'true');
    $self->set_forks   if ($self->forks_count > 0);
    $self->clone_wiki  if ($self->has_wiki eq 'true');
    $self->save_issues if ($self->has_issues eq 'true');
    
    return $self;
}


1;
__END__
