using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using FileAccessAgent.TestSite.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace FileAccessAgent.TestSite.Controllers;

public class HomeController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly FileAccessAgentTestOptions _fileAccessAgentOptions;
    private readonly TestSiteAuthOptions _authOptions;

    public HomeController(
        IHttpClientFactory httpClientFactory,
        IOptions<FileAccessAgentTestOptions> fileAccessAgentOptions,
        IOptions<TestSiteAuthOptions> authOptions)
    {
        _httpClientFactory = httpClientFactory;
        _fileAccessAgentOptions = fileAccessAgentOptions.Value;
        _authOptions = authOptions.Value;
    }

    [HttpGet]
    public async Task<IActionResult> Index()
    {
        var claims = User.Claims
            .Select(claim => new KeyValuePair<string, string>(claim.Type, claim.Value))
            .OrderBy(item => item.Key, StringComparer.Ordinal)
            .ToList();

        var accessToken = await HttpContext.GetTokenAsync("access_token");
        var idToken = await HttpContext.GetTokenAsync("id_token");

        var model = new AuthDebugViewModel
        {
            IsAuthenticated = User.Identity?.IsAuthenticated == true,
            Name = User.Identity?.Name,
            Subject = User.FindFirst("sub")?.Value,
            AccessToken = accessToken,
            IdToken = idToken,
            Claims = claims,
            AccessTokenPayload = DecodeJwtPayload(accessToken),
            LastOperationName = TempData["LastOperationName"]?.ToString(),
            LastOperationStatusCode = TempData["LastOperationStatusCode"]?.ToString(),
            LastOperationResponseBody = TempData["LastOperationResponseBody"]?.ToString(),
            CurrentObjectKey = TempData.Peek("CurrentObjectKey")?.ToString()
        };

        return View(model);
    }

    [HttpGet("auth/login")]
    public IActionResult Login([FromQuery] string? returnUrl = "/")
    {
        if (User.Identity?.IsAuthenticated == true)
        {
            return LocalRedirect(SafeReturnUrl(returnUrl));
        }

        var properties = new AuthenticationProperties
        {
            RedirectUri = SafeReturnUrl(returnUrl)
        };

        return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
    }

    [HttpPost("auth/logout")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Logout([FromForm] string? returnUrl = "/")
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

        var callbackUrl = Url.ActionLink(nameof(LogoutCallback), "Home");
        if (string.IsNullOrWhiteSpace(callbackUrl))
        {
            SaveOperationResult("Logout", (null, "Unable to build logout callback URL."));
            return RedirectToAction(nameof(Index));
        }

        var logoutUrl = $"{TrimSlash(_authOptions.WebBaseUrl)}/account/logout?returnUrl={Uri.EscapeDataString(callbackUrl)}";
        return Redirect(logoutUrl);
    }

    [HttpGet("auth/logout-callback")]
    public async Task<IActionResult> LogoutCallback()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return RedirectToAction(nameof(Index));
    }

    [HttpPost("test/upload")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> UploadFile([FromForm] string? objectKey, [FromForm] IFormFile? file)
    {
        if (file is null || file.Length <= 0)
        {
            SaveOperationResult("Upload", (null, "Please select a file."));
            return RedirectToAction(nameof(Index));
        }

        var resolvedObjectKey = string.IsNullOrWhiteSpace(objectKey)
            ? BuildDefaultObjectKey(file.FileName)
            : objectKey.Trim();

        var result = await SendAgentRequestAsync(
            "Upload",
            resolvedObjectKey,
            async (client, encodedObjectKey, cancellationToken) =>
            {
                await using var stream = file.OpenReadStream();
                using var body = new StreamContent(stream);
                body.Headers.ContentLength = file.Length;
                var mediaType = string.IsNullOrWhiteSpace(file.ContentType) ? "application/octet-stream" : file.ContentType;
                body.Headers.ContentType = new MediaTypeHeaderValue(mediaType);

                using var request = new HttpRequestMessage(HttpMethod.Put, $"/files/{encodedObjectKey}")
                {
                    Content = body
                };
                return await client.SendAsync(request, cancellationToken);
            });

        SetCurrentObjectKey(TryExtractObjectKeyFromResponse(result.Body) ?? resolvedObjectKey);
        SaveOperationResult("Upload", result);
        return RedirectToAction(nameof(Index));
    }

    [HttpPost("test/metadata")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> GetMetadata([FromForm] string objectKey)
    {
        SetCurrentObjectKey(objectKey);
        var result = await SendAgentRequestAsync(
            "Metadata",
            objectKey,
            (client, encodedObjectKey, cancellationToken) => client.GetAsync($"/files/metadata/{encodedObjectKey}", cancellationToken));

        SaveOperationResult("Metadata", result);
        return RedirectToAction(nameof(Index));
    }

    [HttpPost("test/delete")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteFile([FromForm] string objectKey)
    {
        SetCurrentObjectKey(objectKey);
        var result = await SendAgentRequestAsync(
            "Delete",
            objectKey,
            (client, encodedObjectKey, cancellationToken) => client.DeleteAsync($"/files/{encodedObjectKey}", cancellationToken));

        SaveOperationResult("Delete", result);
        return RedirectToAction(nameof(Index));
    }

    [HttpPost("test/head")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> HeadFile([FromForm] string objectKey)
    {
        SetCurrentObjectKey(objectKey);
        var result = await SendAgentRequestAsync(
            "Head",
            objectKey,
            async (client, encodedObjectKey, cancellationToken) =>
            {
                using var request = new HttpRequestMessage(HttpMethod.Head, $"/files/{encodedObjectKey}");
                return await client.SendAsync(request, cancellationToken);
            });

        SaveOperationResult("Head", result);
        return RedirectToAction(nameof(Index));
    }

    [HttpPost("test/health")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Health()
    {
        if (!Uri.TryCreate(_fileAccessAgentOptions.BaseUrl, UriKind.Absolute, out var baseUri))
        {
            SaveOperationResult("Health", (null, "FileAccessAgent:BaseUrl is invalid."));
            return RedirectToAction(nameof(Index));
        }

        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = baseUri;

        try
        {
            using var response = await client.GetAsync("/health", HttpContext.RequestAborted);
            var body = await response.Content.ReadAsStringAsync(HttpContext.RequestAborted);
            SaveOperationResult("Health", ((int)response.StatusCode, body));
            return RedirectToAction(nameof(Index));
        }
        catch (Exception ex)
        {
            SaveOperationResult("Health", (null, $"Health request failed: {ex.Message}"));
            return RedirectToAction(nameof(Index));
        }
    }

    [HttpPost("test/download")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DownloadFile([FromForm] string objectKey)
    {
        if (string.IsNullOrWhiteSpace(objectKey))
        {
            SaveOperationResult("Download", (null, "objectKey is required."));
            return RedirectToAction(nameof(Index));
        }

        SetCurrentObjectKey(objectKey);
        var issue = await IssueDelegatedDownloadTokenAsync(objectKey);
        if (!issue.Success || string.IsNullOrWhiteSpace(issue.Token))
        {
            SaveOperationResult("Download", (issue.StatusCode, issue.Body));
            return RedirectToAction(nameof(Index));
        }

        if (!Uri.TryCreate(_fileAccessAgentOptions.BaseUrl, UriKind.Absolute, out var baseUri))
        {
            SaveOperationResult("Download", (null, "FileAccessAgent:BaseUrl is invalid."));
            return RedirectToAction(nameof(Index));
        }

        var encodedObjectKey = EncodeObjectKey(objectKey);
        var agentClient = _httpClientFactory.CreateClient();
        agentClient.BaseAddress = baseUri;
        agentClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", issue.Token);

        try
        {
            using var response = await agentClient.GetAsync($"/files/{encodedObjectKey}", HttpContext.RequestAborted);
            var bodyPreview = await ReadBodyPreviewAsync(response);
            var summary = $"issue_status={issue.StatusCode ?? 0}\nissue_response={issue.Body}\n\nagent_status={(int)response.StatusCode}\nagent_response_preview={bodyPreview}";
            SaveOperationResult("Download", ((int)response.StatusCode, summary));
            return RedirectToAction(nameof(Index));
        }
        catch (Exception ex)
        {
            SaveOperationResult("Download", (null, $"Download request failed: {ex.Message}"));
            return RedirectToAction(nameof(Index));
        }
    }

    [HttpPost("test/download-file")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DownloadFileDirect([FromForm] string objectKey)
    {
        if (string.IsNullOrWhiteSpace(objectKey))
        {
            SaveOperationResult("DownloadFile", (null, "objectKey is required."));
            return RedirectToAction(nameof(Index));
        }

        SetCurrentObjectKey(objectKey);
        var issue = await IssueDelegatedDownloadTokenAsync(objectKey);
        if (!issue.Success || string.IsNullOrWhiteSpace(issue.Token))
        {
            SaveOperationResult("DownloadFile", (issue.StatusCode, issue.Body));
            return RedirectToAction(nameof(Index));
        }

        if (!Uri.TryCreate(_fileAccessAgentOptions.BaseUrl, UriKind.Absolute, out var baseUri))
        {
            SaveOperationResult("DownloadFile", (null, "FileAccessAgent:BaseUrl is invalid."));
            return RedirectToAction(nameof(Index));
        }

        var encodedObjectKey = EncodeObjectKey(objectKey);
        var directUrl = new Uri(baseUri, $"/files/{encodedObjectKey}?access_token={Uri.EscapeDataString(issue.Token!)}").ToString();
        return Redirect(directUrl);
    }

    [HttpPost("test/download-invalid-token")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DownloadWithInvalidToken([FromForm] string objectKey, [FromForm] string? token)
    {
        if (string.IsNullOrWhiteSpace(objectKey))
        {
            SaveOperationResult("DownloadInvalidToken", (null, "objectKey is required."));
            return RedirectToAction(nameof(Index));
        }

        SetCurrentObjectKey(objectKey);
        if (!Uri.TryCreate(_fileAccessAgentOptions.BaseUrl, UriKind.Absolute, out var baseUri))
        {
            SaveOperationResult("DownloadInvalidToken", (null, "FileAccessAgent:BaseUrl is invalid."));
            return RedirectToAction(nameof(Index));
        }

        var badToken = string.IsNullOrWhiteSpace(token)
            ? $"invalid-{Guid.NewGuid():N}"
            : token.Trim();

        var encodedObjectKey = EncodeObjectKey(objectKey);
        var agentClient = _httpClientFactory.CreateClient();
        agentClient.BaseAddress = baseUri;
        agentClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", badToken);

        try
        {
            using var response = await agentClient.GetAsync($"/files/{encodedObjectKey}", HttpContext.RequestAborted);
            var bodyPreview = await ReadBodyPreviewAsync(response);
            var summary = $"token={badToken}\nagent_status={(int)response.StatusCode}\nagent_response_preview={bodyPreview}";
            SaveOperationResult("DownloadInvalidToken", ((int)response.StatusCode, summary));
            return RedirectToAction(nameof(Index));
        }
        catch (Exception ex)
        {
            SaveOperationResult("DownloadInvalidToken", (null, $"Download invalid token request failed: {ex.Message}"));
            return RedirectToAction(nameof(Index));
        }
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }

    private static IReadOnlyList<KeyValuePair<string, string>> DecodeJwtPayload(string? token)
    {
        if (string.IsNullOrWhiteSpace(token))
        {
            return [];
        }

        var handler = new JwtSecurityTokenHandler();
        if (!handler.CanReadToken(token))
        {
            return [];
        }

        var jwt = handler.ReadJwtToken(token);
        return jwt.Claims
            .Select(claim => new KeyValuePair<string, string>(claim.Type, claim.Value))
            .OrderBy(item => item.Key, StringComparer.Ordinal)
            .ToList();
    }

    private static string SafeReturnUrl(string? returnUrl)
    {
        if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith('/'))
        {
            return "/";
        }

        return returnUrl;
    }

    private static string TrimSlash(string value)
    {
        return string.IsNullOrWhiteSpace(value) ? string.Empty : value.TrimEnd('/');
    }

    private async Task<(int? StatusCode, string Body)> SendAgentRequestAsync(
        string operation,
        string objectKey,
        Func<HttpClient, string, CancellationToken, Task<HttpResponseMessage>> sendAsync)
    {
        if (string.IsNullOrWhiteSpace(objectKey))
        {
            return (null, "objectKey is required.");
        }

        var serviceTokenResult = await GetServiceAccessTokenAsync();
        if (!serviceTokenResult.Success || string.IsNullOrWhiteSpace(serviceTokenResult.Token))
        {
            return (serviceTokenResult.StatusCode, serviceTokenResult.Body);
        }

        if (!Uri.TryCreate(_fileAccessAgentOptions.BaseUrl, UriKind.Absolute, out var baseUri))
        {
            return (null, "FileAccessAgent:BaseUrl is invalid.");
        }

        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = baseUri;
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", serviceTokenResult.Token);
        var encodedObjectKey = EncodeObjectKey(objectKey);

        try
        {
            using var response = await sendAsync(client, encodedObjectKey, HttpContext.RequestAborted);
            var body = await response.Content.ReadAsStringAsync(HttpContext.RequestAborted);
            return ((int)response.StatusCode, body);
        }
        catch (Exception ex)
        {
            return (null, $"{operation} request failed: {ex.Message}");
        }
    }

    private async Task<(bool Success, string? Token, int? StatusCode, string Body)> IssueDelegatedDownloadTokenAsync(string objectKey)
    {
        var serviceTokenResult = await GetServiceAccessTokenAsync();
        if (!serviceTokenResult.Success || string.IsNullOrWhiteSpace(serviceTokenResult.Token))
        {
            return (false, null, serviceTokenResult.StatusCode, serviceTokenResult.Body);
        }

        if (!Uri.TryCreate(_fileAccessAgentOptions.BaseUrl, UriKind.Absolute, out _))
        {
            return (false, null, null, "FileAccessAgent:BaseUrl is invalid.");
        }

        var userIdRaw = User.FindFirst("sub")?.Value;
        if (_fileAccessAgentOptions.TenantId == Guid.Empty)
        {
            return (false, null, null, "FileAccessAgent:TenantId is required.");
        }

        if (!Guid.TryParse(userIdRaw, out var userId))
        {
            return (false, null, null, "Current token sub claim is not a GUID user id.");
        }

        if (!Uri.TryCreate(_authOptions.Authority, UriKind.Absolute, out var memberCenterBase))
        {
            return (false, null, null, "MemberCenter:Authority is invalid.");
        }

        var payload = new
        {
            tenant_id = _fileAccessAgentOptions.TenantId,
            user_id = userId,
            object_key = objectKey,
            method = "GET",
            expires_in_seconds = 300
        };

        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = memberCenterBase;
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", serviceTokenResult.Token);

        try
        {
            using var response = await client.PostAsJsonAsync(
                _authOptions.DelegatedDownloadTokenEndpointPath,
                payload,
                HttpContext.RequestAborted);
            var body = await response.Content.ReadAsStringAsync(HttpContext.RequestAborted);
            if (!response.IsSuccessStatusCode)
            {
                return (false, null, (int)response.StatusCode, body);
            }

            var token = ExtractTokenFromIssueResponse(body);
            if (string.IsNullOrWhiteSpace(token))
            {
                return (false, null, (int)response.StatusCode, $"Cannot find token in response: {body}");
            }

            return (true, token, (int)response.StatusCode, body);
        }
        catch (Exception ex)
        {
            return (false, null, null, $"Issue delegated token failed: {ex.Message}");
        }
    }

    private async Task<(bool Success, string? Token, int? StatusCode, string Body)> GetServiceAccessTokenAsync()
    {
        if (string.IsNullOrWhiteSpace(_authOptions.ServiceClientId) || string.IsNullOrWhiteSpace(_authOptions.ServiceClientSecret))
        {
            return (false, null, null, "MemberCenter:ServiceClientId and ServiceClientSecret are required.");
        }

        if (!Uri.TryCreate(_authOptions.Authority, UriKind.Absolute, out var memberCenterBase))
        {
            return (false, null, null, "MemberCenter:Authority is invalid.");
        }

        var form = new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
            ["client_id"] = _authOptions.ServiceClientId,
            ["client_secret"] = _authOptions.ServiceClientSecret,
            ["scope"] = _authOptions.ServiceScopes
        };

        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = memberCenterBase;

        try
        {
            using var response = await client.PostAsync(
                _authOptions.ServiceTokenEndpointPath,
                new FormUrlEncodedContent(form),
                HttpContext.RequestAborted);

            var body = await response.Content.ReadAsStringAsync(HttpContext.RequestAborted);
            if (!response.IsSuccessStatusCode)
            {
                return (false, null, (int)response.StatusCode, body);
            }

            var token = ExtractTokenFromIssueResponse(body);
            if (string.IsNullOrWhiteSpace(token))
            {
                return (false, null, (int)response.StatusCode, $"Cannot find access_token in response: {body}");
            }

            return (true, token, (int)response.StatusCode, body);
        }
        catch (Exception ex)
        {
            return (false, null, null, $"Service token request failed: {ex.Message}");
        }
    }

    private void SaveOperationResult(string operationName, (int? StatusCode, string Body) result)
    {
        TempData["LastOperationName"] = operationName;
        TempData["LastOperationStatusCode"] = result.StatusCode?.ToString() ?? "N/A";
        TempData["LastOperationResponseBody"] = string.IsNullOrWhiteSpace(result.Body) ? "<empty>" : result.Body;
    }

    private void SetCurrentObjectKey(string? objectKey)
    {
        if (!string.IsNullOrWhiteSpace(objectKey))
        {
            TempData["CurrentObjectKey"] = objectKey.Trim();
        }
    }

    private static string EncodeObjectKey(string objectKey)
    {
        var segments = objectKey
            .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(Uri.EscapeDataString);
        return string.Join('/', segments);
    }

    private static string? ExtractTokenFromIssueResponse(string json)
    {
        if (string.IsNullOrWhiteSpace(json))
        {
            return null;
        }

        try
        {
            using var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;

            if (root.ValueKind != JsonValueKind.Object)
            {
                return null;
            }

            if (root.TryGetProperty("token", out var token) && token.ValueKind == JsonValueKind.String)
            {
                return token.GetString();
            }

            if (root.TryGetProperty("access_token", out var accessToken) && accessToken.ValueKind == JsonValueKind.String)
            {
                return accessToken.GetString();
            }

            return null;
        }
        catch
        {
            return null;
        }
    }

    private static string? TryExtractObjectKeyFromResponse(string json)
    {
        if (string.IsNullOrWhiteSpace(json))
        {
            return null;
        }

        try
        {
            using var doc = JsonDocument.Parse(json);
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                return null;
            }

            if (root.TryGetProperty("object_key", out var objectKey) && objectKey.ValueKind == JsonValueKind.String)
            {
                return objectKey.GetString();
            }

            return null;
        }
        catch
        {
            return null;
        }
    }

    private static string BuildDefaultObjectKey(string fileName)
    {
        var safeName = SanitizeFileName(fileName);
        return $"demo/uploads/{DateTime.UtcNow:yyyyMMddHHmmss}-{safeName}";
    }

    private static string SanitizeFileName(string fileName)
    {
        var name = Path.GetFileName(string.IsNullOrWhiteSpace(fileName) ? "upload.bin" : fileName);
        foreach (var invalid in Path.GetInvalidFileNameChars())
        {
            name = name.Replace(invalid, '_');
        }

        return name;
    }

    private static async Task<string> ReadBodyPreviewAsync(HttpResponseMessage response)
    {
        var bytes = await response.Content.ReadAsByteArrayAsync();
        if (bytes.Length == 0)
        {
            return "<empty>";
        }

        var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
        if (contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)
            || contentType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
        {
            return Encoding.UTF8.GetString(bytes);
        }

        var preview = Convert.ToBase64String(bytes.Length > 128 ? bytes[..128] : bytes);
        return $"binary({bytes.Length} bytes), base64_preview={preview}";
    }
}
