using LibGit2Sharp;
using LibGit2Sharp.Handlers;

namespace RackPeek.Domain.Git;

public interface IGitCredentialsProvider {
    CredentialsHandler GetHandler();
}

/// <summary>
/// HTTP Basic auth using a personal access token as the password. Works with
/// any forge that accepts a token over HTTPS — GitHub, Gitea, GitLab, Bitbucket,
/// Forgejo, etc.
/// </summary>
public sealed class TokenCredentialsProvider(string username, string token) : IGitCredentialsProvider {
    private readonly string _username = username ?? throw new ArgumentNullException(nameof(username));
    private readonly string _token = token ?? throw new ArgumentNullException(nameof(token));

    public CredentialsHandler GetHandler() {
        return (_, _, _) => new UsernamePasswordCredentials {
            Username = _username,
            Password = _token
        };
    }
}

public sealed class LibGit2GitRepository : IGitRepository {
    private readonly string _configDirectory;
    private readonly CredentialsHandler _credentials;
    private readonly CertificateCheckHandler? _certificateCheck;

    public LibGit2GitRepository(
        string configDirectory,
        IGitCredentialsProvider credentialsProvider,
        bool insecureTls = false) {
        _configDirectory = configDirectory;
        _credentials = credentialsProvider.GetHandler();
        // The user opted into git by setting GIT_TOKEN. Auto-init on a fresh
        // config directory so the UI can immediately offer Add Remote — without
        // this, first-time users hit "Git is not available." on every action.
        // Init is idempotent for existing repos (IsValid skips the call) and
        // does not touch existing files; it only creates .git/. Failure (e.g.
        // a read-only mount) must not throw out of the singleton factory — the
        // UI relies on IsAvailable=false to render the writability warning.
        if (Directory.Exists(configDirectory) && !Repository.IsValid(configDirectory))
            try {
                Repository.Init(configDirectory);
            }
            catch {
                // Leave IsAvailable=false; surfaced via the writability warning.
            }

        _isAvailable = Repository.IsValid(configDirectory);
        // When insecureTls is true, accept any TLS certificate. Required for
        // self-hosted forges (Gitea, GitLab) behind a private CA or self-signed
        // cert. Public hosts already ship trusted certs; leave it off for them.
        InsecureTls = insecureTls;
        _certificateCheck = insecureTls
            ? (_, _, _) => true
            : null;
    }

    private FetchOptions BuildFetchOptions() => new() {
        CredentialsProvider = _credentials,
        CertificateCheck = _certificateCheck
    };

    private PushOptions BuildPushOptions() => new() {
        CredentialsProvider = _credentials,
        CertificateCheck = _certificateCheck
    };

    private bool _isAvailable;

    public bool IsAvailable => _isAvailable;
    public bool InsecureTls { get; }

    public void Init() {
        Repository.Init(_configDirectory);

        _isAvailable = true;
    }

    private Repository OpenRepo() => new(_configDirectory);

    private static Signature GetSignature(Repository repo) {
        var name = repo.Config.Get<string>("user.name")?.Value ?? "RackPeek";
        var email = repo.Config.Get<string>("user.email")?.Value ?? "rackpeek@local";

        return new Signature(name, email, DateTimeOffset.Now);
    }

    private static Remote GetRemote(Repository repo)
        => repo.Network.Remotes["origin"] ?? repo.Network.Remotes.First();

    public GitRepoStatus GetStatus() {
        if (!_isAvailable)
            return GitRepoStatus.NotAvailable;

        using Repository repo = OpenRepo();

        return repo.RetrieveStatus().IsDirty
            ? GitRepoStatus.Dirty
            : GitRepoStatus.Clean;
    }

    public void StageAll() {
        using Repository repo = OpenRepo();

        var files = repo.RetrieveStatus()
            .Where(e => e.State != FileStatus.Ignored)
            .Select(e => e.FilePath)
            .ToList();

        if (files.Count == 0)
            return;

        Commands.Stage(repo, files);
    }

    public void Commit(string message) {
        using Repository repo = OpenRepo();

        Signature signature = GetSignature(repo);
        repo.Commit(message, signature, signature);
    }

    public string GetDiff() {
        using Repository repo = OpenRepo();

        Tree? tree = repo.Head.Tip?.Tree;

        Patch patch = repo.Diff.Compare<Patch>(
            tree,
            DiffTargets.Index | DiffTargets.WorkingDirectory);

        return patch?.Content ?? string.Empty;
    }
    public string[] GetChangedFiles() {
        using Repository repo = OpenRepo();

        return repo.RetrieveStatus()
            .Where(e => e.State != FileStatus.Ignored)
            .Select(e => $"{GetPrefix(e.State)}  {e.FilePath}")
            .ToArray();
    }

    private static string GetPrefix(FileStatus state) => state switch {
        FileStatus.NewInWorkdir or FileStatus.NewInIndex => "A",
        FileStatus.DeletedFromWorkdir or FileStatus.DeletedFromIndex => "D",
        FileStatus.RenamedInWorkdir or FileStatus.RenamedInIndex => "R",
        _ when state.HasFlag(FileStatus.ModifiedInWorkdir)
          || state.HasFlag(FileStatus.ModifiedInIndex) => "M",
        _ => "?"
    };

    public void RestoreAll() {
        using Repository repo = OpenRepo();

        repo.CheckoutPaths(
            repo.Head.FriendlyName,
            ["*"],
            new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force });

        repo.RemoveUntrackedFiles();
    }

    public string GetCurrentBranch() {
        using Repository repo = OpenRepo();
        return repo.Head.FriendlyName;
    }

    public GitLogEntry[] GetLog(int count) {
        using Repository repo = OpenRepo();

        if (repo.Head.Tip is null)
            return [];

        return repo.Commits
            .Take(count)
            .Select(c => new GitLogEntry(
                c.Sha[..7],
                c.MessageShort,
                c.Author.Name,
                FormatRelativeDate(c.Author.When)))
            .ToArray();
    }

    public bool HasRemote() {
        using Repository repo = OpenRepo();
        return repo.Network.Remotes.Any();
    }

    public GitSyncStatus FetchAndGetSyncStatus() {
        using Repository repo = OpenRepo();

        if (!repo.Network.Remotes.Any())
            return new GitSyncStatus(0, 0, false);

        Remote remote = GetRemote(repo);

        Commands.Fetch(
            repo,
            remote.Name,
            remote.FetchRefSpecs.Select(r => r.Specification),
            BuildFetchOptions(),
            null);

        // If the repo has no commits yet (unborn branch)
        if (repo.Head.Tip == null)
            return new GitSyncStatus(0, 0, true);

        Branch? remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];

        if (remoteBranch?.Tip == null)
            return new GitSyncStatus(repo.Commits.Count(), 0, true);

        HistoryDivergence? divergence = repo.ObjectDatabase.CalculateHistoryDivergence(
            repo.Head.Tip,
            remoteBranch.Tip);

        return new GitSyncStatus(
            divergence.AheadBy ?? 0,
            divergence.BehindBy ?? 0,
            true);
    }
    public void Push() {
        using Repository repo = OpenRepo();

        Remote remote = GetRemote(repo);
        var branch = repo.Head.FriendlyName;
        var refSpec = $"refs/heads/{branch}:refs/heads/{branch}";

        try {
            repo.Network.Push(
                remote,
                refSpec,
                BuildPushOptions());
        }
        catch (NonFastForwardException) {
            PullInternal(repo);

            repo.Network.Push(
                remote,
                refSpec,
                BuildPushOptions());
        }

        if (repo.Head.TrackedBranch is null) {
            repo.Branches.Update(repo.Head,
                b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{branch}");
        }
    }

    public void Pull() {
        using Repository repo = OpenRepo();
        PullInternal(repo);
    }

    private void PullInternal(Repository repo) {
        if (!repo.Network.Remotes.Any())
            return;

        Remote remote = GetRemote(repo);

        Commands.Fetch(
            repo,
            remote.Name,
            remote.FetchRefSpecs.Select(r => r.Specification),
            BuildFetchOptions(),
            null);

        Branch? remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];

        if (remoteBranch?.Tip == null)
            return;

        // hard reset to remote branch
        repo.Reset(ResetMode.Hard, remoteBranch.Tip);

        repo.Branches.Update(repo.Head,
            b => b.TrackedBranch = remoteBranch.CanonicalName);
    }
    public void AddRemote(string name, string url) {
        using Repository repo = OpenRepo();

        if (repo.Network.Remotes[name] != null)
            return;

        repo.Network.Remotes.Add(name, url);

        Remote remote = repo.Network.Remotes[name];

        // fetch remote state
        Commands.Fetch(
            repo,
            remote.Name,
            remote.FetchRefSpecs.Select(r => r.Specification),
            BuildFetchOptions(),
            null);

        // detect if remote has a default branch
        Branch? remoteMain =
            repo.Branches[$"{remote.Name}/main"] ??
            repo.Branches[$"{remote.Name}/master"];

        var hasLocalFiles =
            repo.RetrieveStatus()
                .Any(e => e.State != FileStatus.Ignored);

        // CASE 1: remote repo already has commits
        if (remoteMain != null && remoteMain.Tip != null) {
            Branch local = repo.CreateBranch(remoteMain.FriendlyName, remoteMain.Tip);
            Commands.Checkout(repo, local);

            repo.Branches.Update(local,
                b => b.TrackedBranch = remoteMain.CanonicalName);

            if (hasLocalFiles) {
                // import existing config to a new branch
                var importBranchName = $"rackpeek-{DateTime.UtcNow:yyyyMMddHHmmss}";

                Branch importBranch = repo.CreateBranch(importBranchName);
                Commands.Checkout(repo, importBranch);

                Commands.Stage(repo, "*");

                Signature sig = GetSignature(repo);

                repo.Commit(
                    "rackpeek: import existing config",
                    sig,
                    sig);

                repo.Network.Push(
                    remote,
                    $"refs/heads/{importBranchName}:refs/heads/{importBranchName}",
                    BuildPushOptions());

                repo.Branches.Update(importBranch,
                    b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{importBranchName}");
            }

            return;
        }

        // CASE 2: remote repo is empty
        if (hasLocalFiles) {
            var branchName = "main";

            Branch branch = repo.CreateBranch(branchName);
            Commands.Checkout(repo, branch);

            Commands.Stage(repo, "*");

            Signature sig = GetSignature(repo);

            repo.Commit(
                "rackpeek: initial config",
                sig,
                sig);

            repo.Network.Push(
                remote,
                $"refs/heads/{branchName}:refs/heads/{branchName}",
                BuildPushOptions());

            repo.Branches.Update(branch,
                b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{branchName}");
        }
    }

    private static string FormatRelativeDate(DateTimeOffset date) {
        TimeSpan diff = DateTimeOffset.Now - date;

        if (diff.TotalMinutes < 1) return "just now";
        if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} minutes ago";
        if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} hours ago";
        if (diff.TotalDays < 30) return $"{(int)diff.TotalDays} days ago";
        if (diff.TotalDays < 365) return $"{(int)(diff.TotalDays / 30)} months ago";

        return $"{(int)(diff.TotalDays / 365)} years ago";
    }
}
