#!/usr/bin/perl
# gott - Game of Trees companion tool
# Shortcuts for daily got workflows + git interoperability
#
# Copyright (c) 2026 Luciano Federico Pereira <lucianopereira@posteo.es>
# SPDX-License-Identifier: BSD-2-Clause

use strict;
use warnings;
use File::Basename qw(basename dirname);
use File::Path    qw(make_path);
use POSIX         qw(strftime);
use Cwd           qw(getcwd);

our $VERSION = '0.3.0';

# ── colour helpers ─────────────────────────────────────────────────────────

sub _c    { -t STDOUT ? "\033[$_[0]m$_[1]\033[0m" : $_[1] }
sub yellow { _c( 33, $_[0] ) }
sub cyan   { _c( 36, $_[0] ) }
sub green  { _c( 32, $_[0] ) }
sub red    { _c( 31, $_[0] ) }
sub bold   { _c( 1,  $_[0] ) }

# ── helpers ────────────────────────────────────────────────────────────────

sub run {
    my (@cmd) = @_;
    system(@cmd) == 0 or die "Command failed: @cmd\n";
}

sub capture {
    my $out = `@_ 2>/dev/null`;
    chomp $out;
    return $out;
}

sub got_required {
    capture('which got') or die "got not found. Install from https://gameoftrees.org\n";
}

sub git_required {
    capture('which git') or die "git not found. Install git to use git-interop commands.\n";
}

sub author_required {
    $ENV{GOT_AUTHOR} or die <<'END';
GOT_AUTHOR is not set.
Export it before using gott, e.g.:
  export GOT_AUTHOR="Your Name <you@example.com>"
END
}

sub in_worktree {
    my $dir = getcwd();
    while ( $dir ne '/' ) {
        return 1 if -d "$dir/.got";
        $dir = dirname($dir);
    }
    return 0;
}

sub worktree_or_die {
    in_worktree() or die "Not inside a got work tree (no .got found).\n";
}

sub repo_path {
    my $dir = getcwd();
    while ( $dir ne '/' ) {
        my $f = "$dir/.got/repository";
        if ( -f $f ) {
            open( my $fh, '<', $f ) or die "Cannot read $f: $!\n";
            my $repo = <$fh>;
            chomp $repo;
            return $repo;
        }
        $dir = dirname($dir);
    }
    die "Cannot find .got/repository\n";
}

sub current_branch {
    my $info = capture('got info 2>/dev/null');
    return $1 if $info =~ /work tree branch:\s*(\S+)/;
    return 'main';
}

sub timestamp { strftime( '%Y%m%d-%H%M%S', localtime ) }

# ── commands ───────────────────────────────────────────────────────────────

sub cmd_help {
    print bold("gott $VERSION") . " — Game of Trees companion\n\n";
    print "Usage: gott <command> [args]\n\n";
    print bold("Local workflow\n");
    print <<'HELP';
  new      <name> [dir]   Init bare repo + work tree, ready to use
  clone    <url>  [dir]   Clone a remote got/git repo and check out
  snap     [msg]          Stage all (got add -R .) then commit
  log                     Pretty colour log
  branches                List branches, highlight current
  switch   <branch>       Switch to branch
  nb       <branch>       New branch and switch to it
  undo     [file]         Revert file (or all) to last commit
  info                    Work tree info + status

HELP
    print bold("Git interoperability\n");
    print <<'HELP';
  git-remote [url]        Show or set the upstream git remote URL
  git-pull                Fetch from git remote into local got repo
  git-push   [branch]     Push got commits upstream to git remote
  sync                    git-pull then rebase current branch on top
  rebase     [base]       Rebase current branch onto base (default: origin/main)
  patch      [n]          Export last n commits as patch files (default 1)
  apply      <file.patch> Apply a patch file to the work tree
  stash                   Save uncommitted changes to a temp branch
  unstash                 Restore most recent stash

HELP
    print bold("Environment\n");
    print "  GOT_AUTHOR   \"Name <email>\"  (required for new/snap/stash)\n\n";
}

# ── local workflow ─────────────────────────────────────────────────────────

sub cmd_new {
    my ( $name, $base_dir ) = @_;
    die "Usage: gott new <name> [dir]\n" unless $name;
    author_required();

    $base_dir //= $ENV{HOME} . '/Repos';
    make_path($base_dir) unless -d $base_dir;

    my $repo = "$base_dir/$name.git";
    my $tree = "$base_dir/$name";
    die "Repo already exists: $repo\n" if -e $repo;
    die "Work tree already exists: $tree\n" if -e $tree;

    make_path($tree);
    print "Creating repo:     $repo\n";
    run( 'got', 'init', $repo );

    # seed with README so repo is non-empty
    my $readme = "$tree/README.md";
    open( my $fh, '>', $readme ) or die "Cannot write $readme: $!\n";
    print $fh "# $name\n\nCreated with gott on " . strftime( '%Y-%m-%d', localtime ) . "\n";
    close $fh;

    print "Importing seed commit...\n";
    {
        my $cwd = getcwd();
        chdir($tree) or die "Cannot chdir $tree: $!\n";
        run( 'got', 'import', '-m', 'Initial import', '-r', $repo, '.' );
        chdir($cwd);
    }
    unlink $readme;

    print "Checking out into: $tree\n";
    run( 'got', 'checkout', $repo, $tree );
    print "\nDone. Work tree ready at: " . green($tree) . "\n";
    print "cd $tree\n";
}

sub cmd_clone {
    my ( $url, $dir ) = @_;
    die "Usage: gott clone <url> [dir]\n" unless $url;

    $dir //= basename($url);
    $dir =~ s/\.git$//;
    my $repo = "$dir.git";

    print "Cloning $url -> $repo\n";
    run( 'got', 'clone', $url, $repo );
    print "Checking out into $dir\n";
    run( 'got', 'checkout', $repo, $dir );
    print "\nDone. cd " . green($dir) . "\n";
}

sub cmd_snap {
    my ($msg) = @_;
    worktree_or_die();
    author_required();
    $msg //= 'snap ' . timestamp();
    print "Staging all files...\n";
    run( 'got', 'add', '-R', '.' );
    print "Committing: " . cyan($msg) . "\n";
    run( 'got', 'commit', '-m', $msg );
}

sub cmd_log {
    worktree_or_die();
    open( my $fh, '-|', 'got', 'log' ) or die "Cannot run got log: $!\n";
    while (<$fh>) {
        if    (/^commit\s+/)      { print yellow($_) }
        elsif (/^(Author|Date):/) { print cyan($_)   }
        else                      { print $_          }
    }
    close $fh;
}

sub cmd_branches {
    worktree_or_die();
    my @branches = split /\n/, capture('got branch');
    my $cur = current_branch();
    for my $b (@branches) {
        print( $b eq $cur ? green("* $b") . "\n" : "  $b\n" );
    }
}

sub cmd_switch {
    my ($branch) = @_;
    die "Usage: gott switch <branch>\n" unless $branch;
    worktree_or_die();
    run( 'got', 'update', '-b', $branch );
}

sub cmd_nb {
    my ($branch) = @_;
    die "Usage: gott nb <branch>\n" unless $branch;
    worktree_or_die();
    run( 'got', 'branch', '-c', $branch );
    run( 'got', 'update', '-b', $branch );
    print "Switched to new branch " . green($branch) . "\n";
}

sub cmd_undo {
    my ($file) = @_;
    worktree_or_die();
    if ($file) {
        print "Reverting $file\n";
        run( 'got', 'revert', $file );
    } else {
        print "Reverting all changes\n";
        run( 'got', 'revert', '.' );
    }
}

sub cmd_info {
    worktree_or_die();
    run( 'got', 'info' );
    print "\n";
    run( 'got', 'status' );
}

# ── git interop ────────────────────────────────────────────────────────────

sub _remote_file { repo_path() . '/gott-remote' }

sub _read_remote {
    my $file = _remote_file();
    die "No git remote set. Run: gott git-remote <url>\n" unless -f $file;
    open( my $fh, '<', $file ) or die "Cannot read $file: $!\n";
    my $url = <$fh>;
    chomp $url;
    return $url;
}

sub cmd_git_remote {
    my ($url) = @_;
    worktree_or_die();
    my $file = _remote_file();
    if ($url) {
        open( my $fh, '>', $file ) or die "Cannot write $file: $!\n";
        print $fh "$url\n";
        close $fh;
        print "Remote set to: " . cyan($url) . "\n";
    } else {
        if ( -f $file ) {
            open( my $fh, '<', $file ) or die $!;
            my $u = <$fh>;
            chomp $u;
            print "Remote: " . cyan($u) . "\n";
        } else {
            print "No remote set. Use: gott git-remote <url>\n";
        }
    }
}

sub cmd_git_pull {
    worktree_or_die();
    git_required();
    my $url  = _read_remote();
    my $repo = repo_path();
    print "Fetching from " . cyan($url) . " ...\n";
    # got fetch works over git:// https:// and ssh://
    run( 'got', 'fetch', '-r', $url, '-R', $repo );
    print green("Fetch complete.") . " Run 'gott rebase' or 'gott sync' to rebase.\n";
}

sub cmd_git_push {
    my ($branch) = @_;
    worktree_or_die();
    git_required();
    my $url = _read_remote();
    $branch //= current_branch();
    print "Pushing " . cyan($branch) . " to " . cyan($url) . "\n";
    run( 'got', 'send', '-b', $branch, '-r', $url );
    print green("Push complete.") . "\n";
}

sub cmd_sync {
    worktree_or_die();
    git_required();
    my $url    = _read_remote();
    my $repo   = repo_path();
    my $branch = current_branch();

    print "Fetching from " . cyan($url) . " ...\n";
    run( 'got', 'fetch', '-r', $url, '-R', $repo );

    print "Rebasing " . cyan($branch) . " on origin/main ...\n";
    my $rc = system( 'got', 'rebase', 'refs/remotes/origin/main' );
    if ( $rc != 0 ) {
        print red("Rebase has conflicts.") . " Resolve, then:\n";
        print "  got rebase -c   # continue\n";
        print "  got rebase -a   # abort\n";
        exit 1;
    }
    print green("Sync complete.") . "\n";
}

sub cmd_rebase {
    my ($new_base) = @_;
    worktree_or_die();
    $new_base //= 'refs/remotes/origin/main';
    my $branch = current_branch();
    print "Rebasing " . cyan($branch) . " onto " . cyan($new_base) . " ...\n";
    my $rc = system( 'got', 'rebase', $new_base );
    if ( $rc != 0 ) {
        print red("Rebase has conflicts.") . " Resolve, then:\n";
        print "  got rebase -c   # continue\n";
        print "  got rebase -a   # abort\n";
        exit 1;
    }
    print green("Rebase complete.") . "\n";
}

sub cmd_patch {
    my ($n) = @_;
    $n //= 1;
    $n =~ /^\d+$/ or die "Usage: gott patch [n]\n";
    worktree_or_die();

    my @hashes;
    open( my $fh, '-|', 'got', 'log' ) or die "Cannot run got log: $!\n";
    while (<$fh>) {
        push @hashes, $1 if /^commit\s+([0-9a-f]+)/;
    }
    close $fh;

    $n = @hashes if $n > @hashes;

    for my $i ( 1 .. $n ) {
        my $hash = $hashes[ $i - 1 ];
        my $file = sprintf( '%04d-%s.patch', $i, substr( $hash, 0, 8 ) );
        print "Writing $file\n";
        open( my $out, '>', $file ) or die "Cannot write $file: $!\n";
        open( my $diff, '-|', 'got', 'diff', '-c', $hash )
            or die "Cannot run got diff: $!\n";
        print $out $_ while <$diff>;
        close $diff;
        close $out;
    }
    print green("Done.") . " Send .patch files; teammates apply with: git am *.patch\n";
}

sub cmd_apply {
    my ($file) = @_;
    die "Usage: gott apply <file.patch>\n" unless $file && -f $file;
    worktree_or_die();
    print "Applying $file ...\n";
    run( 'patch', '-p1', '--input', $file );
    print green("Patch applied.") . " Review, then: gott snap \"apply patch\"\n";
}

sub cmd_stash {
    worktree_or_die();
    author_required();

    my $status = capture('got status');
    if ( !$status ) { print "Nothing to stash.\n"; return; }

    my $branch = current_branch();
    my $stash  = '_stash_' . timestamp();

    print "Stashing to branch " . cyan($stash) . " ...\n";
    run( 'got', 'branch', '-c', $stash );
    run( 'got', 'update', '-b', $stash );
    run( 'got', 'add',    '-R', '.' );
    run( 'got', 'commit', '-m', "stash: $stash" );
    run( 'got', 'update', '-b', $branch );
    run( 'got', 'revert', '.' );

    my $sf = repo_path() . '/gott-stash';
    open( my $fh, '>', $sf ) or die "Cannot write $sf: $!\n";
    print $fh "$stash\n";
    close $fh;

    print green("Stashed.") . " Restore with: gott unstash\n";
}

sub cmd_unstash {
    worktree_or_die();
    my $sf = repo_path() . '/gott-stash';
    die "No stash found. Run 'gott stash' first.\n" unless -f $sf;

    open( my $fh, '<', $sf ) or die "Cannot read $sf: $!\n";
    my $stash = <$fh>;
    chomp $stash;
    close $fh;

    print "Restoring stash from branch " . cyan($stash) . " ...\n";

    # find the stash commit hash
    my $hash = '';
    open( my $log, '-|', 'got', 'log', '-b', $stash )
        or die "Cannot run got log: $!\n";
    while (<$log>) {
        if (/^commit\s+([0-9a-f]+)/) { $hash = $1; last; }
    }
    close $log;
    die "Cannot find stash commit on branch $stash\n" unless $hash;

    run( 'got', 'cherrypick', $hash );
    run( 'got', 'unstage',    '.' );
    run( 'got', 'branch',     '-d', $stash );
    unlink $sf;

    print green("Unstashed.") . " Changes are unstaged — review then: gott snap \"...\"\n";
}

# ── dispatch ───────────────────────────────────────────────────────────────

got_required();

my $cmd = shift @ARGV // 'help';

my %dispatch = (
    new          => \&cmd_new,
    clone        => \&cmd_clone,
    snap         => \&cmd_snap,
    log          => \&cmd_log,
    branches     => \&cmd_branches,
    switch       => \&cmd_switch,
    nb           => \&cmd_nb,
    undo         => \&cmd_undo,
    info         => \&cmd_info,
    'git-remote' => \&cmd_git_remote,
    'git-pull'   => \&cmd_git_pull,
    'git-push'   => \&cmd_git_push,
    sync         => \&cmd_sync,
    rebase       => \&cmd_rebase,
    patch        => \&cmd_patch,
    apply        => \&cmd_apply,
    stash        => \&cmd_stash,
    unstash      => \&cmd_unstash,
    help         => \&cmd_help,
    '--help'     => \&cmd_help,
    '-h'         => \&cmd_help,
);

my $handler = $dispatch{$cmd}
    or do { print STDERR red("Unknown command: $cmd") . "\n\n"; cmd_help(); exit 1; };

eval { $handler->(@ARGV) };
if ($@) {
    chomp( my $err = $@ );
    print STDERR red("Error:") . " $err\n";
    exit 1;
}
