@page "/{Owner}/{Name}"
@page "/{Owner}/{Name}/settings"
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@inject IRepositoryService RepositoryService
@inject IGitService GitService
@inject Forge.Web.Services.MarkdownService MarkdownService
@inject IJSRuntime JS
@inject NavigationManager Navigation
@rendermode InteractiveServer

<PageTitle>@(repo?.Owner)/@(repo?.Name) - Forge</PageTitle>

@if (repo == null)
{
    <p>Loading...</p>
}
else
{
    <div class="repo-header">
        <h1>
            <a href="/@repo.Owner">@repo.Owner</a> / <strong>@repo.Name</strong>
            @if (repo.IsPrivate)
            {
                <span class="badge">Private</span>
            }
        </h1>
        @if (!string.IsNullOrEmpty(repo.Description))
        {
            <p class="description">@repo.Description</p>
        }
    </div>

    <div class="tabs">
        <a href="/@Owner/@Name?tab=files" class="tab @(tab == "files" ? "active" : "")">Files</a>
        <a href="/@Owner/@Name?tab=commits" class="tab @(tab == "commits" ? "active" : "")">Commits</a>
        <a href="/@Owner/@Name?tab=branches" class="tab @(tab == "branches" ? "active" : "")">Branches</a>
        @if (IsOwner)
        {
            <a href="/@Owner/@Name/settings" class="tab @(tab == "settings" ? "active" : "")">Settings</a>
        }
    </div>

    @if (tab == "files")
    {
        @if (files == null || !files.Any())
        {
            <div class="empty-state">
                <p>Empty repository</p>
                <p style="font-size: 0.875rem; color: #8b949e;">Push some code to get started!</p>
            </div>

            <div class="clone-box">
                <h3>Clone</h3>
                <code style="display: block; background: #0d1117; padding: 0.5rem 0.75rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem;">@($"git clone http://{GetBaseUrl()}/{repo.Owner}/{repo.Name}.git")</code>
                
                <h3 style="margin-top: 1rem;">Push an existing repository</h3>
                <div style="background: #0d1117; padding: 0.5rem 0.75rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem; white-space: pre-wrap;">@GetPushCommands()</div>
            </div>
        }
        else
        {
            @if (lastCommit != null)
            {
                <div class="last-commit">
                    <a href="/@repo.Owner/@repo.Name/commit/@lastCommit.Sha" class="commit-message">@lastCommit.Message.Split('\n')[0]</a>
                    <div class="commit-meta">
                        <span><strong>@lastCommit.Author</strong></span>
                        <span>committed @RelativeTime(lastCommit.AuthorDate)</span>
                        <code>@lastCommit.Sha.Substring(0, 7)</code>
                    </div>
                </div>
            }

            <div class="clone-url">
                <span class="clone-label">Clone:</span>
                <code class="clone-command">@($"git clone http://{GetBaseUrl()}/{repo.Owner}/{repo.Name}.git")</code>
                @if (copyFeedback)
                {
                    <span class="copy-feedback">Copied!</span>
                }
                else
                {
                    <button class="clone-copy" @onclick="CopyCloneUrl" title="Copy clone URL">
                        <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
                            <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
                            <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
                        </svg>
                    </button>
                }
            </div>

            <div class="file-search-container">
                <input type="text" 
                       class="file-search-input" 
                       placeholder="Search files (press 't')" 
                       @bind="fileSearchQuery" 
                       @bind:event="oninput" 
                       @onkeydown="HandleSearchKeydown" />
                @if (!string.IsNullOrEmpty(fileSearchQuery))
                {
                    <button class="file-search-clear" @onclick="ClearFileSearch" title="Clear search">✕</button>
                }
            </div>

            <table class="file-list">
                <tbody>
                    @foreach (var f in filteredFiles)
                    {
                        <tr>
                            <td class="icon">@(f.Type == TreeEntryType.Directory ? "📁" : "📄")</td>
                            <td class="name">
                                @if (string.IsNullOrEmpty(fileSearchQuery))
                                {
                                    @if (f.Type == TreeEntryType.Directory)
                                    {
                                        <a href="/@repo.Owner/@repo.Name/tree/@repo.DefaultBranch/@f.Path">@f.Name</a>
                                    }
                                    else
                                    {
                                        <a href="/@repo.Owner/@repo.Name/blob/@repo.DefaultBranch/@f.Path">@f.Name</a>
                                    }
                                }
                                else
                                {
                                    @if (f.Type == TreeEntryType.Directory)
                                    {
                                        <a href="/@repo.Owner/@repo.Name/tree/@repo.DefaultBranch/@f.Path">@HighlightMatch(f.Path, fileSearchQuery)</a>
                                    }
                                    else
                                    {
                                        <a href="/@repo.Owner/@repo.Name/blob/@repo.DefaultBranch/@f.Path">@HighlightMatch(f.Path, fileSearchQuery)</a>
                                    }
                                }
                            </td>
                            <td class="size">@(f.Size.HasValue ? FormatSize(f.Size.Value) : "")</td>
                        </tr>
                    }
                </tbody>
            </table>

            @if (allFiles != null && allFiles.Any() && !filteredFiles.Any() && !string.IsNullOrEmpty(fileSearchQuery))
            {
                <p class="empty-state">No files match your search</p>
            }

            @if (!string.IsNullOrEmpty(readmeHtml.Value))
            {
                <section class="readme-card">
                    <div class="readme-header">
                        <h3>@readmeName</h3>
                    </div>
                    <article class="markdown-body">
                        @readmeHtml
                    </article>
                </section>
            }
        }
    }
    else if (tab == "commits")
    {
        @if (commitList == null || !commitList.Any())
        {
            <p class="empty-state">No commits yet</p>
        }
        else
        {
            @foreach (var c in commitList)
            {
                <div class="commit">
                    <div class="commit-message">
                        <a href="/@repo.Owner/@repo.Name/commit/@c.Sha">@c.Message.Split('\n')[0]</a>
                    </div>
                    <div class="commit-meta">
                        <span>@c.Author</span>
                        <span>committed @RelativeTime(c.AuthorDate)</span>
                        <code>@c.Sha.Substring(0, 7)</code>
                    </div>
                </div>
            }
        }
    }
    else if (tab == "branches")
    {
        @if (branchList == null || !branchList.Any())
        {
            <p class="empty-state">No branches</p>
        }
        else
        {
            @foreach (var b in branchList)
            {
                <div class="branch">
                    <span>🌿</span>
                    <a href="/@repo.Owner/@repo.Name/tree/@b.Name">@b.Name</a>
                    @if (b.IsDefault)
                    {
                        <span class="badge">default</span>
                    }
                    <span class="branch-meta">Updated @RelativeTime(b.LastCommitDate ?? DateTime.MinValue)</span>
                </div>
            }
        }
    }
    else if (tab == "settings")
    {
        @if (!IsOwner)
        {
            <p class="empty-state">You do not have permission to edit this repository.</p>
        }
        else
        {
            @if (!string.IsNullOrEmpty(message))
            {
                <p class="success-message">@message</p>
            }

            @if (!string.IsNullOrEmpty(error))
            {
                <p class="auth-error">@error</p>
            }

            <div class="card settings-card">
                <div class="form-group">
                    <label>Owner</label>
                    <input value="@repo.Owner" disabled />
                </div>

                <div class="form-group">
                    <label>Repository Name</label>
                    <input value="@repo.Name" disabled />
                </div>

                <div class="form-group">
                    <label>Description</label>
                    <input @bind="description" @bind:event="oninput" placeholder="A short description" />
                </div>

                <div class="form-group">
                    <label>
                        <input type="checkbox" @bind="isPrivate" />
                        Private repository
                    </label>
                    <p class="form-help">Private repositories require authentication for clone/fetch and push.</p>
                </div>

                <div style="display: flex; gap: 1rem; margin-top: 1.5rem;">
                    <button type="button" class="btn-primary" @onclick="SaveSettingsAsync">Save Changes</button>
                    <a href="/@repo.Owner/@repo.Name" class="btn-secondary">Back to Repository</a>
                </div>
            </div>
        }
    }
}

@code {
    [Parameter] public string Owner { get; set; } = "";
    [Parameter] public string Name { get; set; } = "";
    [SupplyParameterFromQuery] public string? Tab { get; set; }
    [CascadingParameter] private Task<AuthenticationState>? AuthenticationStateTask { get; set; }

    private Repository? repo;
    private IEnumerable<BranchInfo>? branchList;
    private IEnumerable<TreeNode>? files;
    private IEnumerable<TreeNode>? allFiles;
    private IEnumerable<CommitInfo>? commitList;
    private CommitInfo? lastCommit;
    private IEnumerable<TreeNode> filteredFiles => string.IsNullOrEmpty(fileSearchQuery) 
        ? (files ?? []) 
        : (allFiles ?? []).Where(f => f.Path.Contains(fileSearchQuery, StringComparison.OrdinalIgnoreCase));
    private MarkupString readmeHtml;
    private string? readmeName;
    private string? currentUser;
    private string? description;
    private bool isPrivate;
    private string? message;
    private string? error;
    private string fileSearchQuery = "";
    private bool copyFeedback = false;
    private bool IsOwner => repo != null && string.Equals(currentUser, repo.Owner, StringComparison.Ordinal);
    private bool IsSettingsRoute => Navigation.ToBaseRelativePath(Navigation.Uri).Equals($"{Owner}/{Name}/settings", StringComparison.OrdinalIgnoreCase);
    private string tab => IsSettingsRoute ? "settings" : Tab ?? "files";

    protected override async Task OnParametersSetAsync()
    {
        if (AuthenticationStateTask != null)
        {
            var authState = await AuthenticationStateTask;
            currentUser = authState.User.Identity?.Name;
        }

        repo = await RepositoryService.GetByOwnerAndNameAsync(Owner, Name);
        if (repo != null)
        {
            branchList = await GitService.GetBranchesAsync(repo);
            files = await GitService.GetTreeAsync(repo, repo.DefaultBranch);
            allFiles = await GitService.GetAllFilesAsync(repo, repo.DefaultBranch);
            commitList = await GitService.GetCommitsAsync(repo, repo.DefaultBranch);
            lastCommit = commitList?.FirstOrDefault();
            description = repo.Description;
            isPrivate = repo.IsPrivate;
            await LoadReadmeAsync();
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("forge.initFileSearch", DotNetObjectReference.Create(this));
        }
        
        if (!string.IsNullOrEmpty(readmeHtml.Value))
        {
            await JS.InvokeVoidAsync("forge.highlightCodeBlocks");
        }
    }

    [JSInvokable]
    public void FocusFileSearch()
    {
        fileSearchQuery = "";
        StateHasChanged();
    }

    private void ClearFileSearch()
    {
        fileSearchQuery = "";
    }

    private void HandleSearchKeydown(KeyboardEventArgs e)
    {
        if (e.Key == "Escape")
        {
            fileSearchQuery = "";
            StateHasChanged();
        }
    }

    private MarkupString HighlightMatch(string text, string query)
    {
        if (string.IsNullOrEmpty(query))
            return new MarkupString(System.Web.HttpUtility.HtmlEncode(text));
        
        var index = text.IndexOf(query, StringComparison.OrdinalIgnoreCase);
        if (index < 0)
            return new MarkupString(System.Web.HttpUtility.HtmlEncode(text));
        
        var before = System.Web.HttpUtility.HtmlEncode(text[..index]);
        var match = System.Web.HttpUtility.HtmlEncode(text.Substring(index, query.Length));
        var after = System.Web.HttpUtility.HtmlEncode(text[(index + query.Length)..]);
        
        return new MarkupString($"{before}<mark>{match}</mark>{after}");
    }

    private string GetBaseUrl()
    {
        var uri = Navigation.BaseUri.TrimEnd('/');
        if (uri.StartsWith("https://")) return uri[8..];
        if (uri.StartsWith("http://")) return uri[7..];
        return uri;
    }

    private string GetPushCommands()
    {
        var baseUrl = GetBaseUrl();
        return $"git remote add origin https://user:token@{baseUrl}/{repo!.Owner}/{repo!.Name}.git\n" +
               $"git branch -M main\n" +
               $"git push -u origin main";
    }

    private string GetCloneUrl()
    {
        return $"git clone http://{GetBaseUrl()}/{repo!.Owner}/{repo!.Name}.git";
    }

    private async Task CopyCloneUrl()
    {
        var url = $"http://{GetBaseUrl()}/{repo!.Owner}/{repo!.Name}.git";
        await JS.InvokeVoidAsync("navigator.clipboard.writeText", url);
        copyFeedback = true;
        StateHasChanged();
        
        await Task.Delay(1500);
        copyFeedback = false;
        StateHasChanged();
    }

    private async Task SaveSettingsAsync()
    {
        if (repo == null || !IsOwner)
        {
            error = "You do not have permission to edit this repository.";
            message = null;
            return;
        }

        repo.Description = string.IsNullOrWhiteSpace(description) ? null : description.Trim();
        repo.IsPrivate = isPrivate;

        await RepositoryService.UpdateAsync(repo);

        message = "Repository settings saved.";
        error = null;
    }

    private async Task LoadReadmeAsync()
    {
        readmeHtml = default;
        readmeName = null;

        var readmeEntry = files?.FirstOrDefault(f =>
            f.Type == TreeEntryType.File &&
            (
                string.Equals(f.Name, "README.md", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(f.Name, "README.markdown", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(f.Name, "README", StringComparison.OrdinalIgnoreCase)
            ));

        if (repo == null || readmeEntry == null)
        {
            return;
        }

        var readmeFile = await GitService.GetFileAsync(repo, repo.DefaultBranch, readmeEntry.Path);
        if (string.IsNullOrWhiteSpace(readmeFile?.Content))
        {
            return;
        }

        readmeName = readmeEntry.Name;
        readmeHtml = MarkdownService.Render(readmeFile.Content);
    }

    private static string FormatSize(long bytes)
    {
        string[] s = { "B", "KB", "MB", "GB" };
        int i = 0;
        double size = bytes;
        while (size >= 1024 && i < s.Length - 1) { size /= 1024; i++; }
        return $"{size:0.#} {s[i]}";
    }

    private static string RelativeTime(DateTime date)
    {
        var span = DateTime.UtcNow - date;
        if (span.TotalDays > 365) return $"{(int)(span.TotalDays / 365)}y";
        if (span.TotalDays > 30) return $"{(int)(span.TotalDays / 30)}mo";
        if (span.TotalDays > 1) return $"{(int)span.TotalDays}d";
        if (span.TotalHours > 1) return $"{(int)span.TotalHours}h";
        if (span.TotalMinutes > 1) return $"{(int)span.TotalMinutes}m";
        return "now";
    }
}
An unhandled error has occurred. Reload 🗙