using LibGit2Sharp;
using Forge.Core.Models;
using LibGit2SharpRepository = LibGit2Sharp.Repository;

namespace Forge.Core.Services;

/// <summary>
/// Git operations using LibGit2Sharp
/// </summary>
public class GitService : IGitService
{
    private readonly string _repositoriesRoot;

    public GitService(string repositoriesRoot)
    {
        _repositoriesRoot = repositoriesRoot;
        Directory.CreateDirectory(_repositoriesRoot);
    }

    public async Task<Models.Repository> InitializeRepositoryAsync(string name, string owner, string? description = null, bool isPrivate = false)
    {
        var path = $"{owner}/{name}.git";
        var fullPath = System.IO.Path.Combine(_repositoriesRoot, path);
        
        Directory.CreateDirectory(System.IO.Path.GetDirectoryName(fullPath)!);
        
        // Initialize bare repository
        LibGit2SharpRepository.Init(fullPath, true);

        // Set HEAD to point to main branch (write directly for empty repo)
        File.WriteAllText(System.IO.Path.Combine(fullPath, "HEAD"), "ref: refs/heads/main\n");
        
        var repo = new Models.Repository
        {
            Id = Guid.NewGuid(),
            Name = name,
            Owner = owner,
            Description = description,
            IsPrivate = isPrivate,
            Path = path,
            DefaultBranch = "main"
        };
        
        return await Task.FromResult(repo);
    }

    public async Task<IEnumerable<BranchInfo>> GetBranchesAsync(Models.Repository repository)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return [];
        
        try
        {
            using var repo = new LibGit2SharpRepository(fullPath);
            
            var result = new List<BranchInfo>();
            foreach (var b in repo.Branches)
            {
                if (b.IsRemote && b.RemoteName != "origin")
                    continue;
                
                try
                {
                    result.Add(new BranchInfo
                    {
                        Name = b.FriendlyName,
                        HeadSha = b.Tip?.Sha ?? "",
                        IsDefault = b.FriendlyName == repository.DefaultBranch,
                        LastCommitDate = b.Tip?.Author.When.UtcDateTime,
                        LastCommitMessage = b.Tip?.MessageShort
                    });
                }
                catch
                {
                    // Skip branches that cause errors
                }
            }
            
            return await Task.FromResult(result.OrderByDescending(b => b.LastCommitDate));
        }
        catch (LibGit2SharpException ex)
        {
            Console.WriteLine($"[Forge] Git error in GetBranchesAsync: {ex.Message}");
            return [];
        }
    }

    public async Task<BranchInfo?> GetBranchAsync(Models.Repository repository, string branchName)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return null;
        
        try
        {
            using var repo = new LibGit2SharpRepository(fullPath);
            var branch = repo.Branches[branchName];
            
            if (branch == null)
                return null;
            
            return await Task.FromResult(new BranchInfo
            {
                Name = branch.FriendlyName,
                HeadSha = branch.Tip?.Sha ?? "",
                IsDefault = branch.FriendlyName == repository.DefaultBranch,
                LastCommitDate = branch.Tip?.Author.When.UtcDateTime,
                LastCommitMessage = branch.Tip?.MessageShort
            });
        }
        catch (LibGit2SharpException ex)
        {
            Console.WriteLine($"[Forge] Git error in GetBranchAsync: {ex.Message}");
            return null;
        }
    }

    public async Task<BranchInfo?> GetDefaultBranchAsync(Models.Repository repository)
    {
        return await GetBranchAsync(repository, repository.DefaultBranch);
    }

    public async Task<IEnumerable<TreeNode>?> GetTreeAsync(Models.Repository repository, string branch, string? path = null)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return null;
        
        try
        {
            using var repo = new LibGit2SharpRepository(fullPath);
            
            var commit = ResolveCommitish(repo, branch);
            if (commit == null)
                return null;
            
            var tree = commit.Tree;
            if (tree == null)
                return null;
            
            if (!string.IsNullOrEmpty(path))
            {
                var parts = path.Split('/', '\\');
                foreach (var part in parts)
                {
                    var entry = tree[part];
                    if (entry?.Target is Tree subTree)
                        tree = subTree;
                    else
                        return null; // Path not found or not a directory
                }
            }
            
            // Build list manually to avoid enumeration issues
            var nodes = new List<TreeNode>();
            foreach (var entry in tree)
            {
                long? size = null;
                try
                {
                    if (entry.Target is Blob blob)
                        size = blob.Size;
                }
                catch { /* ignore lookup errors */ }
                
                nodes.Add(new TreeNode
                {
                    Name = entry.Name,
                    Path = string.IsNullOrEmpty(path) ? entry.Name : $"{path}/{entry.Name}",
                    Type = MapEntryType(entry.Mode),
                    Sha = entry.Target.Sha,
                    Size = size
                });
            }
            
            return await Task.FromResult(
                nodes.OrderBy(n => n.Type == TreeEntryType.Directory ? 0 : 1).ThenBy(n => n.Name).ToList()
            );
        }
        catch (LibGit2SharpException ex)
        {
            Console.WriteLine($"[Forge] Git error in GetTreeAsync: {ex.Message}");
            return null;
        }
    }

    public async Task<IEnumerable<TreeNode>> GetAllFilesAsync(Models.Repository repository, string branch)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return [];
        
        try
        {
            using var repo = new LibGit2SharpRepository(fullPath);
            
            var commit = ResolveCommitish(repo, branch);
            if (commit == null)
                return [];
            
            var nodes = new List<TreeNode>();
            CollectAllFiles(commit.Tree, "", nodes);
            
            return await Task.FromResult(nodes.OrderBy(n => n.Path).ToList());
        }
        catch (LibGit2SharpException ex)
        {
            Console.WriteLine($"[Forge] Git error in GetAllFilesAsync: {ex.Message}");
            return [];
        }
    }

    private static void CollectAllFiles(Tree tree, string basePath, List<TreeNode> nodes)
    {
        foreach (var entry in tree)
        {
            var path = string.IsNullOrEmpty(basePath) ? entry.Name : $"{basePath}/{entry.Name}";
            
            if (entry.Target is Tree subTree)
            {
                CollectAllFiles(subTree, path, nodes);
            }
            else if (entry.Target is Blob blob)
            {
                long? size = null;
                try { size = blob.Size; } catch { }
                
                nodes.Add(new TreeNode
                {
                    Name = entry.Name,
                    Path = path,
                    Type = TreeEntryType.File,
                    Sha = blob.Sha,
                    Size = size
                });
            }
        }
    }

    public async Task<TreeNode?> GetFileAsync(Models.Repository repository, string branch, string path)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return null;
        
        using var repo = new LibGit2SharpRepository(fullPath);
        
        var commit = ResolveCommitish(repo, branch);
        if (commit == null)
            return null;
        
        var entry = commit[path];
        if (entry == null || entry.Target is not Blob blob)
            return null;
        
        var content = blob.GetContentText();
        
        return await Task.FromResult(new TreeNode
        {
            Name = System.IO.Path.GetFileName(path),
            Path = path,
            Type = TreeEntryType.File,
            Sha = blob.Sha,
            Size = blob.Size,
            Content = content
        });
    }

    public async Task<IEnumerable<CommitInfo>> GetCommitsAsync(Models.Repository repository, string branch, int skip = 0, int take = 50)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return [];
        
        try
        {
            using var repo = new LibGit2SharpRepository(fullPath);
            
            var commit = ResolveCommitish(repo, branch);
            if (commit == null)
                return [];
            
            var commits = repo.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = commit })
                .Skip(skip)
                .Take(take)
                .Select(c => new CommitInfo
                {
                    Sha = c.Sha,
                    Message = c.Message,
                    Author = c.Author.Name,
                    AuthorEmail = c.Author.Email,
                    AuthorDate = c.Author.When.UtcDateTime,
                    Committer = c.Committer.Name,
                    CommitterEmail = c.Committer.Email,
                    CommitterDate = c.Committer.When.UtcDateTime,
                    ParentSha = c.Parents.FirstOrDefault()?.Sha
                })
                .ToList();
            
            return await Task.FromResult(commits);
        }
        catch (LibGit2SharpException ex)
        {
            Console.WriteLine($"[Forge] Git error in GetCommitsAsync: {ex.Message}");
            return [];
        }
    }

    public async Task<CommitDetail?> GetCommitAsync(Models.Repository repository, string sha)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return null;
        
        using var repo = new LibGit2SharpRepository(fullPath);
        
        var commit = repo.Lookup<LibGit2Sharp.Commit>(sha);
        if (commit == null)
            return null;
        
        var changes = new List<FileChange>();
        
        // Compare with parent to get changes
        var parent = commit.Parents.FirstOrDefault();
        if (parent != null)
        {
            var diff = repo.Diff.Compare<Patch>(parent.Tree, commit.Tree);
            
            foreach (var patchEntry in diff)
            {
                var changeType = patchEntry.Status switch
                {
                    ChangeKind.Added => ChangeType.Added,
                    ChangeKind.Deleted => ChangeType.Deleted,
                    ChangeKind.Modified => ChangeType.Modified,
                    ChangeKind.Renamed => ChangeType.Renamed,
                    _ => ChangeType.Modified
                };
                
                // Count additions and deletions from patch
                var patch = patchEntry.Patch ?? "";
                var additions = 0;
                var deletions = 0;
                foreach (var line in patch.Split('\n'))
                {
                    if (line.StartsWith('+') && !line.StartsWith("++")) additions++;
                    else if (line.StartsWith('-') && !line.StartsWith("--")) deletions++;
                }
                
                changes.Add(new FileChange
                {
                    Path = patchEntry.Path,
                    ChangeType = changeType,
                    Additions = additions,
                    Deletions = deletions,
                    Diff = patch
                });
            }
        }
        else
        {
            // Initial commit - all files are added
            foreach (var entry in commit.Tree)
            {
                changes.AddRange(GetAllFilesWithContent(entry, "", commit));
            }
        }
        
        return await Task.FromResult(new CommitDetail
        {
            Sha = commit.Sha,
            Message = commit.Message,
            Author = commit.Author.Name,
            AuthorEmail = commit.Author.Email,
            AuthorDate = commit.Author.When.UtcDateTime,
            Committer = commit.Committer.Name,
            CommitterEmail = commit.Committer.Email,
            CommitterDate = commit.Committer.When.UtcDateTime,
            ParentSha = commit.Parents.FirstOrDefault()?.Sha,
            Changes = changes
        });
    }

    public bool RepositoryExists(Models.Repository repository)
    {
        var fullPath = GetFullPath(repository);
        return Directory.Exists(fullPath);
    }

    public void EnsureRepositoryExists(Models.Repository repository)
    {
        var fullPath = GetFullPath(repository);
        if (Directory.Exists(fullPath))
            return;

        // Recreate the bare repository
        Directory.CreateDirectory(System.IO.Path.GetDirectoryName(fullPath)!);
        LibGit2SharpRepository.Init(fullPath, true);
        File.WriteAllText(System.IO.Path.Combine(fullPath, "HEAD"), "ref: refs/heads/main\n");
        Console.WriteLine($"[Forge] Recreated missing repository: {repository.Owner}/{repository.Name}");
    }

    public Task<int> ValidateAndRepairRepositoriesAsync(IEnumerable<Models.Repository> repositories)
    {
        int repaired = 0;
        foreach (var repo in repositories)
        {
            if (!RepositoryExists(repo))
            {
                EnsureRepositoryExists(repo);
                repaired++;
            }
        }
        return Task.FromResult(repaired);
    }

    public async Task<IEnumerable<BlameLine>> GetBlameAsync(Models.Repository repository, string branch, string path)
    {
        var fullPath = GetFullPath(repository);
        if (!Directory.Exists(fullPath))
            return [];
        
        try
        {
            using var repo = new LibGit2SharpRepository(fullPath);
            
            var commit = ResolveCommitish(repo, branch);
            if (commit == null)
                return [];
            
            var entry = commit[path];
            if (entry == null || entry.Target is not Blob blob)
                return [];
            
            var blame = repo.Blame(path, new BlameOptions { StartingAt = commit });
            var content = blob.GetContentText();
            var lines = content.Split('\n');
            
            var result = new List<BlameLine>();
            
            for (int i = 0; i < lines.Length; i++)
            {
                var lineNum = i + 1;
                var lineContent = lines[i];
                
                // Find the blame hunk for this line
                var hunk = blame.FirstOrDefault(h =>
                    h.FinalStartLineNumber <= lineNum &&
                    lineNum < h.FinalStartLineNumber + h.LineCount);
                
                if (hunk != null)
                {
                    var blamedCommit = hunk.FinalCommit;
                    result.Add(new BlameLine
                    {
                        LineNumber = lineNum,
                        Content = lineContent,
                        CommitSha = blamedCommit.Sha,
                        Author = hunk.FinalSignature.Name,
                        Date = hunk.FinalSignature.When.UtcDateTime,
                        Message = blamedCommit.MessageShort
                    });
                }
                else
                {
                    // Fallback for lines without blame info
                    result.Add(new BlameLine
                    {
                        LineNumber = lineNum,
                        Content = lineContent,
                        CommitSha = commit.Sha,
                        Author = commit.Author.Name,
                        Date = commit.Author.When.UtcDateTime,
                        Message = commit.MessageShort
                    });
                }
            }
            
            return await Task.FromResult(result);
        }
        catch (LibGit2SharpException ex)
        {
            Console.WriteLine($"[Forge] Git error in GetBlameAsync: {ex.Message}");
            return [];
        }
    }

    private string GetFullPath(Models.Repository repository)
    {
        return System.IO.Path.Combine(_repositoriesRoot, repository.Path);
    }

    private static LibGit2Sharp.Commit? ResolveCommitish(LibGit2SharpRepository repo, string revision)
    {
        var branch = repo.Branches[revision];
        if (branch?.Tip != null)
        {
            return branch.Tip;
        }

        return repo.Lookup<LibGit2Sharp.Commit>(revision);
    }

    private static TreeEntryType MapEntryType(Mode mode)
    {
        return mode switch
        {
            Mode.Directory => TreeEntryType.Directory,
            Mode.SymbolicLink => TreeEntryType.Symlink,
            _ => TreeEntryType.File
        };
    }

    private static IEnumerable<FileChange> GetAllFilesWithContent(TreeEntry entry, string basePath, LibGit2Sharp.Commit commit)
    {
        var path = string.IsNullOrEmpty(basePath) ? entry.Name : $"{basePath}/{entry.Name}";
        
        if (entry.Target is Tree tree)
        {
            return tree.SelectMany(e => GetAllFilesWithContent(e, path, commit));
        }
        
        // Get file content for initial commit diff
        var content = entry.Target is Blob blob ? blob.GetContentText() : "";
        var lines = content.Split('\n').Length;
        
        return [new FileChange
        {
            Path = path,
            ChangeType = ChangeType.Added,
            Additions = lines,
            Deletions = 0,
            Diff = string.IsNullOrEmpty(content) ? null : content.Split('\n').Select(l => $"+{l}").Aggregate((a, b) => $"{a}\n{b}")
        }];
    }

    private static IEnumerable<FileChange> GetAllFiles(TreeEntry entry, string basePath)
    {
        var path = string.IsNullOrEmpty(basePath) ? entry.Name : $"{basePath}/{entry.Name}";
        
        if (entry.Target is Tree tree)
        {
            return tree.SelectMany(e => GetAllFiles(e, path));
        }
        
        return [new FileChange
        {
            Path = path,
            ChangeType = ChangeType.Added,
            Additions = 0,
            Deletions = 0
        }];
    }
}
An unhandled error has occurred. Reload 🗙