// Copyright (c) 2025 TerraByte Inc.
//
// This script contains the core business logic for interacting with the Git
// repository. All public methods are asynchronous and return a promise.

using System;
using System.IO;
using LibGit2Sharp;
using UnityEngine;
using Terra.Arbitrator.Promises;
using System.Collections.Generic;

namespace Terra.Arbitrator.Services
{
    public static class GitService
    {
        // Public method that returns the promise
        public static IPromise<List<GitChange>> CompareLocalToRemote()
        {
            return new Promise<List<GitChange>>(CompareExecutor);
        }
        
        // Public method that returns the promise
        public static IPromise<string> CommitAndPush(List<GitChange> changesToCommit, string commitMessage)
        {
            // Use a lambda here to pass arguments to the executor method
            return new Promise<string>((resolve, reject) => 
                CommitAndPushExecutor(resolve, reject, changesToCommit, commitMessage));
        }
        
        // Creates a promise to revert a single file to its HEAD revision.
        public static IPromise<string> ResetFileChanges(GitChange changeToReset)
        {
            return new Promise<string>((resolve, reject) => 
                ResetFileExecutor(resolve, reject, changeToReset));
        }
        
        // Creates a promise to get the diff patch for a single file against HEAD.
        public static IPromise<string> GetFileDiff(GitChange change)
        {
            return new Promise<string>((resolve, reject) => GetFileDiffExecutor(resolve, reject, change));
        }

        /// <summary>
        /// The private logic for the CompareLocalToRemote promise.
        /// This code is executed on a background thread by the Promise constructor.
        /// </summary>
        private static void CompareExecutor(Action<List<GitChange>> resolve, Action<Exception> reject)
        {
            try
            {
                var changes = new List<GitChange>();
                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
                using var repo = new Repository(projectRoot);

                var remote = repo.Network.Remotes["origin"];
                if (remote == null) throw new Exception("No remote named 'origin' was found.");
                
                Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator fetch");
                
                var remoteBranch = repo.Head.TrackedBranch;
                if (remoteBranch == null || !remoteBranch.IsRemote)
                {
                    throw new Exception($"Current branch '{repo.Head.FriendlyName}' is not tracking a remote branch.");
                }

                var diff = repo.Diff.Compare<TreeChanges>(remoteBranch.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory);

                foreach (var entry in diff)
                {
                    if (entry.Status is ChangeKind.Added or ChangeKind.Deleted or ChangeKind.Modified or ChangeKind.Renamed)
                    {
                        changes.Add(new GitChange(entry.Path, entry.OldPath, entry.Status));
                    }
                }
                
                resolve(changes); // Success
            }
            catch (Exception ex)
            {
                reject(ex); // Failure
            }
        }

        /// <summary>
        /// The private logic for the CommitAndPush promise.
        /// This code is executed on a background thread.
        /// </summary>
        private static void CommitAndPushExecutor(Action<string> resolve, Action<Exception> reject, List<GitChange> changesToCommit, string commitMessage)
        {
            try
            {
                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
                using var repo = new Repository(projectRoot);

                var remote = repo.Network.Remotes["origin"];
                if (remote == null) throw new Exception("No remote named 'origin' found.");
                
                Commands.Fetch(repo, remote.Name, Array.Empty<string>(), new FetchOptions(), "Arbitrator pre-push fetch");

                var trackingDetails = repo.Head.TrackingDetails;
                if (trackingDetails.BehindBy > 0)
                {
                    throw new Exception($"Push aborted. There are {trackingDetails.BehindBy.Value} incoming changes on the remote. Please pull first.");
                }

                foreach (var change in changesToCommit)
                {
                    if (change.Status == ChangeKind.Deleted) Commands.Remove(repo, change.FilePath);
                    else Commands.Stage(repo, change.FilePath);
                }

                var author = new Signature("Arbitrator Tool User", "arbitrator@terabyte.com", DateTimeOffset.Now);
                repo.Commit(commitMessage, author, author);

                repo.Network.Push(repo.Head, new PushOptions());
                
                resolve("Successfully committed and pushed changes!"); // Success
            }
            catch (Exception ex)
            {
                reject(ex); // Failure
            }
        }
        
        /// <summary>
        /// The private logic for resetting a file's changes.
        /// This is executed on a background thread.
        /// </summary>
        private static void ResetFileExecutor(Action<string> resolve, Action<Exception> reject, GitChange changeToReset)
        {
            try
            {
                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
                using var repo = new Repository(projectRoot);

                // For 'Added' files, checking out doesn't remove them. We need to unstage and delete.
                if (changeToReset.Status == ChangeKind.Added)
                {
                    // Unstage the file from the index.
                    Commands.Unstage(repo, changeToReset.FilePath);
                    
                    // And delete it from the working directory.
                    if (projectRoot != null)
                    {
                        var fullPath = Path.Combine(projectRoot, changeToReset.FilePath);
                        if (File.Exists(fullPath))
                        {
                            File.Delete(fullPath);
                        }
                    }
                }
                else if (changeToReset.Status == ChangeKind.Renamed)
                {
                    // 1. Unstage the new path
                    Commands.Unstage(repo, changeToReset.FilePath);
                    // 2. Delete the new file from the working directory
                    if (projectRoot != null)
                    {
                        var newFullPath = Path.Combine(projectRoot, changeToReset.FilePath);
                        if (File.Exists(newFullPath)) File.Delete(newFullPath);
                    }

                    // 3. Checkout the old path from the HEAD to restore it
                    repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.OldFilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
                }
                else
                {
                    // For Modified or Deleted files, CheckoutPaths is the correct command to revert them.
                    repo.CheckoutPaths(repo.Head.Tip.Sha, new[] { changeToReset.FilePath }, new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });
                }

                resolve($"Successfully reset changes for '{changeToReset.FilePath}'");
            }
            catch (Exception ex)
            {
                reject(ex);
            }
        }
        
        private static void GetFileDiffExecutor(Action<string> resolve, Action<Exception> reject, GitChange change)
        {
            try
            {
                var projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
                using var repo = new Repository(projectRoot);

                // Compare the HEAD tree with the working directory.
                var patch = repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory);
                
                // Find the specific change in the complete patch by its path.
                // For renames, the 'Path' property holds the new path, which is what we need to look up.
                var patchEntry = patch[change.FilePath];

                if (patchEntry == null)
                {
                    // This might happen if the change is in the index but not the working dir.
                    // Fallback to checking the index as well.
                    patch = repo.Diff.Compare<Patch>(repo.Head.Tip.Tree, DiffTargets.Index);
                    patchEntry = patch[change.FilePath];
                }

                resolve(patchEntry?.Patch ?? $"No textual changes detected for {change.FilePath}. Status: {change.Status}");
            }
            catch(Exception ex)
            {
                reject(ex);
            }
        }
    }
}
