using AgentSmith.Contracts.Providers;
using AgentSmith.Domain.Entities;
using AgentSmith.Domain.Exceptions;
using AgentSmith.Domain.Models;
using Microsoft.Extensions.Logging;
using Microsoft.TeamFoundation.SourceControl.WebApi;
using Microsoft.VisualStudio.Services.Common;
using Repository = AgentSmith.Domain.Entities.Repository;

namespace AgentSmith.Infrastructure.Services.Providers.Source;

/// <summary>
/// Azure DevOps source provider. CheckoutAsync is metadata-only — the git clone
/// happens sandbox-side via Step{Kind=Run, Command=git, ...}. Default-branch
/// resolution via DevOps REST stays (it is metadata, not git plumbing).
/// </summary>
public sealed class AzureReposSourceProvider(
    string organizationUrl,
    string project,
    string repoName,
    string personalAccessToken,
    IAzDoClientFactory clientFactory,
    ILogger<AzureReposSourceProvider> logger) : ISourceProvider, IPrCommentProvider
{
    private readonly string _organizationUrl = organizationUrl.TrimEnd('/');
    private readonly string _cloneUrl = $"{organizationUrl.TrimEnd('/')}/{project}/_git/{repoName}";
    private string? _cachedDefaultBranch;

    public string ProviderType => "AzureRepos";

    public async Task<Repository> CheckoutAsync(BranchName? branch, CancellationToken cancellationToken)
    {
        var target = branch ?? new BranchName(await GetDefaultBranchAsync(cancellationToken));
        logger.LogInformation(
            "Resolved metadata for {Url} on branch {Branch}", _cloneUrl, target);
        return new Repository(target, _cloneUrl);
    }

    public async Task<string> CreatePullRequestAsync(
        Repository repository, string title, string description,
        CancellationToken cancellationToken,
        TicketId? linkedTicketId = null)
    {
        var client = CreateGitClient();
        var src = $"refs/heads/{repository.CurrentBranch.Value}";
        var tgt = $"refs/heads/{await GetDefaultBranchAsync(cancellationToken)}";
        var pr = new GitPullRequest
        {
            Title = title, Description = description,
            SourceRefName = src, TargetRefName = tgt
        };

        try
        {
            var created = await client.CreatePullRequestAsync(
                pr, project, repoName, cancellationToken: cancellationToken);
            logger.LogInformation("Pull request created: {Url}", BuildPrUrl(created.PullRequestId));

            // Native AzDO work-item-to-PR link via WorkItem relations. Done after
            // create so a relation failure does not block PR creation — operators
            // can manually link later, the PR is the load-bearing artifact.
            if (linkedTicketId is not null
                && int.TryParse(linkedTicketId.Value, out var workItemId))
            {
                await LinkWorkItemToPrAsync(workItemId, created.PullRequestId, cancellationToken);
            }

            return BuildPrUrl(created.PullRequestId);
        }
        catch (Exception ex) when (ex.Message.Contains("TF401179") || ex.Message.Contains("already exists"))
        {
            return await FindExistingPrUrlAsync(client, src, tgt, cancellationToken);
        }
    }

    private async Task LinkWorkItemToPrAsync(int workItemId, int pullRequestId, CancellationToken ct)
    {
        try
        {
            // AzDO relation between a Work Item and a Pull Request uses the
            // `ArtifactLink` rel with a `vstfs:///Git/PullRequestId/...` artifact uri.
            // The uri encodes project-guid / repo-guid / pr-id; resolve the GUIDs
            // first so the relation lands on the right artifact.
            var gitClient = CreateGitClient();
            var repo = await gitClient.GetRepositoryAsync(project, repoName, cancellationToken: ct);
            var projectId = repo.ProjectReference.Id;
            var repoId = repo.Id;
            var artifactUri = $"vstfs:///Git/PullRequestId/{projectId}%2F{repoId}%2F{pullRequestId}";

            var witClient = clientFactory.CreateWorkItemClient(_organizationUrl, personalAccessToken);
            var patch = new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchDocument
            {
                new Microsoft.VisualStudio.Services.WebApi.Patch.Json.JsonPatchOperation
                {
                    Operation = Microsoft.VisualStudio.Services.WebApi.Patch.Operation.Add,
                    Path = "/relations/-",
                    Value = new
                    {
                        rel = "ArtifactLink",
                        url = artifactUri,
                        attributes = new { name = "Pull Request" }
                    }
                }
            };
            await witClient.UpdateWorkItemAsync(patch, workItemId, cancellationToken: ct);
            logger.LogInformation(
                "Linked work item #{WorkItem} to PR !{PrId}", workItemId, pullRequestId);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex,
                "Failed to link work item #{WorkItem} to PR !{PrId} — PR is created, link is missing",
                workItemId, pullRequestId);
        }
    }

    public async Task PostCommentAsync(
        string prIdentifier, string markdown, CancellationToken cancellationToken = default)
    {
        var prId = int.Parse(prIdentifier);
        var gitClient = await CreateConnectionAsync(cancellationToken);
        var thread = new GitPullRequestCommentThread
        {
            Comments = [new Comment { Content = markdown, CommentType = CommentType.Text }],
            Status = CommentThreadStatus.Active
        };
        await gitClient.CreateThreadAsync(thread, project, repoName, prId, cancellationToken: cancellationToken);
        logger.LogInformation("Posted comment on PR #{PrId}", prId);
    }

    private async Task<string> GetDefaultBranchAsync(CancellationToken cancellationToken)
    {
        if (_cachedDefaultBranch is not null)
            return _cachedDefaultBranch;

        try
        {
            var client = CreateGitClient();
            var repo = await client.GetRepositoryAsync(
                project, repoName, cancellationToken: cancellationToken);
            var raw = repo.DefaultBranch ?? "refs/heads/main";
            _cachedDefaultBranch = raw.StartsWith("refs/heads/", StringComparison.Ordinal)
                ? raw["refs/heads/".Length..] : raw;
            logger.LogDebug("Resolved default branch from Azure DevOps API: {Branch}", _cachedDefaultBranch);
            return _cachedDefaultBranch;
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to resolve default branch from Azure DevOps API, falling back to 'main'");
            _cachedDefaultBranch = "main";
            return _cachedDefaultBranch;
        }
    }

    private async Task<string> FindExistingPrUrlAsync(
        GitHttpClient client, string src, string tgt, CancellationToken cancellationToken)
    {
        var criteria = new GitPullRequestSearchCriteria
            { SourceRefName = src, TargetRefName = tgt, Status = PullRequestStatus.Active };
        var existing = (await client.GetPullRequestsAsync(
            project, repoName, criteria, cancellationToken: cancellationToken)).FirstOrDefault()
            ?? throw new ProviderException(ProviderType, "PR already exists but could not be found.");
        logger.LogInformation("Found existing pull request: {Url}", BuildPrUrl(existing.PullRequestId));
        return BuildPrUrl(existing.PullRequestId);
    }

    private string BuildPrUrl(int prId) =>
        $"{_organizationUrl}/{project}/_git/{repoName}/pullrequest/{prId}";

    private GitHttpClient CreateGitClient() =>
        clientFactory.CreateGitClient(_organizationUrl, personalAccessToken);

    private Task<GitHttpClient> CreateConnectionAsync(CancellationToken cancellationToken) =>
        clientFactory.CreateGitClientAsync(_organizationUrl, personalAccessToken, cancellationToken);

    public async Task<string?> TryReadFileAsync(string path, CancellationToken cancellationToken)
    {
        var client = CreateGitClient();
        var branch = await GetDefaultBranchAsync(cancellationToken);
        var descriptor = new GitVersionDescriptor
        {
            Version = branch,
            VersionType = GitVersionType.Branch
        };
        try
        {
            using var stream = await client.GetItemContentAsync(
                project: project,
                repositoryId: repoName,
                path: path,
                versionDescriptor: descriptor,
                cancellationToken: cancellationToken);
            using var reader = new StreamReader(stream);
            return await reader.ReadToEndAsync(cancellationToken);
        }
        catch (VssServiceException ex) when (ex.Message.Contains("could not be found", StringComparison.OrdinalIgnoreCase)
                                          || ex.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }
    }
}
