#!/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.4.0';

# ── configuration ───────────────────────────────────────────────────────────
# Change DEFAULT_REPO_DIR to use a different base directory for new repos.

our $DEFAULT_REPO_DIR = $ENV{HOME} . '/Repos';

# ── 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 ) }

# ── command definitions ────────────────────────────────────────────────────
# To add a command: add an entry here, then write a cmd_foo sub below.
# Fields: name, group, usage (args), desc (one line), code (sub ref)
# Groups: 'local' or 'git'

my @COMMANDS = (
    # name          group   usage                  description
    { name => 'new',        group => 'local', usage => '<name> [dir]',   desc => 'Init bare repo + work tree, ready to use',          code => \&cmd_new        },
    { name => 'clone',      group => 'local', usage => '<url> [dir]',    desc => 'Clone a remote got/git repo and check out',         code => \&cmd_clone      },
    { name => 'snap',       group => 'local', usage => '[msg]',           desc => 'Stage all (got add -R .) then commit',              code => \&cmd_snap       },
    { name => 'log',        group => 'local', usage => '',                desc => 'Pretty colour log',                                 code => \&cmd_log        },
    { name => 'branches',   group => 'local', usage => '',                desc => 'List branches, highlight current',                  code => \&cmd_branches   },
    { name => 'switch',     group => 'local', usage => '<branch>',        desc => 'Switch to branch',                                  code => \&cmd_switch     },
    { name => 'nb',         group => 'local', usage => '<branch>',        desc => 'New branch and switch to it',                      code => \&cmd_nb         },
    { name => 'undo',       group => 'local', usage => '[file]',          desc => 'Revert file (or all) to last commit',               code => \&cmd_undo       },
    { name => 'info',       group => 'local', usage => '',                desc => 'Work tree info + status',                           code => \&cmd_info       },
    { name => 'git-remote', group => 'git',   usage => '[url]',           desc => 'Show or set the upstream git remote URL',           code => \&cmd_git_remote },
    { name => 'git-pull',   group => 'git',   usage => '',                desc => 'Fetch from git remote into local got repo',         code => \&cmd_git_pull   },
    { name => 'git-push',   group => 'git',   usage => '[branch]',        desc => 'Push got commits upstream to git remote',           code => \&cmd_git_push   },
    { name => 'sync',       group => 'git',   usage => '',                desc => 'git-pull then rebase current branch on top',        code => \&cmd_sync       },
    { name => 'rebase',     group => 'git',   usage => '[base]',          desc => 'Rebase current branch onto base (default: origin/main)', code => \&cmd_rebase },
    { name => 'patch',      group => 'git',   usage => '[n]',             desc => 'Export last n commits as patch files (default 1)', code => \&cmd_patch      },
    { name => 'apply',      group => 'git',   usage => '<file.patch>',    desc => 'Apply a patch file to the work tree',               code => \&cmd_apply      },
    { name => 'stash',      group => 'git',   usage => '',                desc => 'Save uncommitted changes to a temp branch',         code => \&cmd_stash      },
    { name => 'unstash',    group => 'git',   usage => '',                desc => 'Restore most recent stash',                         code => \&cmd_unstash    },
);

# ── help (auto-generated from @COMMANDS) ───────────────────────────────────

sub cmd_help {
    print bold("gott $VERSION") . " — Game of Trees companion\n\n";
    print "Usage: gott <command> [args]\n\n";

    my %groups = ( local => 'Local workflow', git => 'Git interoperability' );
    for my $group ( 'local', 'git' ) {
        print bold( $groups{$group} ) . "\n";
        for my $c ( grep { $_->{group} eq $group } @COMMANDS ) {
            my $left = sprintf( '  %-10s %-18s', $c->{name}, $c->{usage} );
            print "$left $c->{desc}\n";
        }
        print "\n";
    }

    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 //= $DEFAULT_REPO_DIR;
    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 _write_file {
    my ( $path, $content ) = @_;
    open( my $fh, '>', $path ) or die "Cannot write $path: $!\n";
    print $fh "$content\n";
    close $fh;
}

sub _read_file {
    my ($path) = @_;
    open( my $fh, '<', $path ) or die "Cannot read $path: $!\n";
    my $line = <$fh>;
    chomp $line;
    return $line;
}

sub _read_remote {
    my $file = _remote_file();
    die "No git remote set. Run: gott git-remote <url>\n" unless -f $file;
    return _read_file($file);
}

sub _do_rebase {
    my ($base) = @_;
    my $rc = system( 'got', 'rebase', $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;
    }
}

sub cmd_git_remote {
    my ($url) = @_;
    worktree_or_die();
    my $file = _remote_file();
    if ($url) {
        _write_file( $file, $url );
        print "Remote set to: " . cyan($url) . "\n";
    } else {
        if ( -f $file ) {
            print "Remote: " . cyan( _read_file($file) ) . "\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";
    _do_rebase('refs/remotes/origin/main');
    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";
    _do_rebase($new_base);
    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', '.' );

    _write_file( repo_path() . '/gott-stash', $stash );

    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;

    my $stash = _read_file($sf);

    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 = (
    ( map { $_->{name} => $_->{code} } @COMMANDS ),
    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;
}
