using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using MemberCenter.Web.Authorization;
using MemberCenter.Web.Models.Admin;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;

namespace MemberCenter.Web.Areas.Admin.Controllers;

[Area("Admin")]
[Authorize(Policy = "Admin")]
[AdminPermission(AdminPermissions.OAuthClients)]
[Route("admin/oauth-clients")]
public class OAuthClientsController : Controller
{
    private readonly IOpenIddictApplicationManager _applicationManager;
    private readonly ITenantService _tenantService;
    private readonly IAuthResourceRegistryService _authResourceRegistry;

    public OAuthClientsController(
        IOpenIddictApplicationManager applicationManager,
        ITenantService tenantService,
        IAuthResourceRegistryService authResourceRegistry)
    {
        _applicationManager = applicationManager;
        _tenantService = tenantService;
        _authResourceRegistry = authResourceRegistry;
    }

    [HttpGet("")]
    [AdminPermission(AdminPermissions.OAuthClientsIndex)]
    public async Task<IActionResult> Index()
    {
        var results = new List<object>();
        await foreach (var application in _applicationManager.ListAsync())
        {
            var properties = await _applicationManager.GetPropertiesAsync(application);
            var usage = properties.TryGetValue("usage", out var usageElement)
                ? usageElement.GetString()
                : "tenant_api";

            results.Add(new
            {
                id = await _applicationManager.GetIdAsync(application),
                name = await _applicationManager.GetDisplayNameAsync(application),
                client_id = await _applicationManager.GetClientIdAsync(application),
                client_type = await _applicationManager.GetClientTypeAsync(application),
                usage,
                redirect_uris = await _applicationManager.GetRedirectUrisAsync(application)
            });
        }

        return View(results);
    }

    [HttpGet("create")]
    [AdminPermission(AdminPermissions.OAuthClientsCreate)]
    public async Task<IActionResult> Create()
    {
        var tenants = await _tenantService.ListAsync();
        return View(new OAuthClientFormViewModel
        {
            Tenants = tenants
        });
    }

    [HttpPost("create")]
    [AdminPermission(AdminPermissions.OAuthClientsCreate)]
    public async Task<IActionResult> Create(OAuthClientFormViewModel model)
    {
        if (!IsValidUsage(model.Usage))
        {
            ModelState.AddModelError(nameof(model.Usage), "Usage must be tenant_api, send_api, web_login, webhook_outbound, platform_service, or file_api.");
        }

        if (!IsTenantOptionalUsage(model.Usage) && (!model.TenantId.HasValue || model.TenantId.Value == Guid.Empty))
        {
            ModelState.AddModelError(nameof(model.TenantId), "Tenant is required for this usage.");
        }

        if (RequiresClientCredentials(model.Usage)
            && !string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
        {
            ModelState.AddModelError(nameof(model.ClientType), "Client type must be confidential for this usage.");
        }

        var redirectUris = NormalizeRedirectUris(model.RedirectUris, out var redirectUriError);
        if (!string.IsNullOrWhiteSpace(redirectUriError))
        {
            ModelState.AddModelError(nameof(model.RedirectUris), redirectUriError);
        }

        if (UsesAuthorizationCodeFlow(model.Usage) && redirectUris.Count == 0)
        {
            ModelState.AddModelError(nameof(model.RedirectUris), "Redirect URI is required for web_login or webhook_outbound usage.");
        }

        if (!ModelState.IsValid)
        {
            model.Tenants = await _tenantService.ListAsync();
            return View(model);
        }

        var clientId = Guid.NewGuid().ToString("N");
        var clientSecret = string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)
            ? GenerateClientSecret()
            : null;

        var descriptor = await BuildDescriptorAsync(clientId, model.Name, model.ClientType, model.Usage);
        if (!string.IsNullOrWhiteSpace(clientSecret))
        {
            descriptor.ClientSecret = clientSecret;
        }

        foreach (var uri in redirectUris)
        {
            descriptor.RedirectUris.Add(new Uri(uri));
        }

        if (!IsTenantOptionalUsage(model.Usage) && model.TenantId.HasValue)
        {
            descriptor.Properties["tenant_id"] = System.Text.Json.JsonSerializer.SerializeToElement(model.TenantId.Value.ToString());
        }

        descriptor.Properties["usage"] = System.Text.Json.JsonSerializer.SerializeToElement(model.Usage);

        await _applicationManager.CreateAsync(descriptor);

        TempData["CreatedClientId"] = clientId;
        if (!string.IsNullOrWhiteSpace(clientSecret))
        {
            TempData["CreatedClientSecret"] = clientSecret;
        }

        return RedirectToAction("Index");
    }

    [HttpGet("edit/{id}")]
    [AdminPermission(AdminPermissions.OAuthClientsEdit)]
    public async Task<IActionResult> Edit(string id)
    {
        var app = await _applicationManager.FindByIdAsync(id);
        if (app is null)
        {
            return NotFound();
        }

        var redirectUris = await _applicationManager.GetRedirectUrisAsync(app);
        var properties = await _applicationManager.GetPropertiesAsync(app);
        var tenantId = properties.TryGetValue("tenant_id", out var value) ? value.GetString() : string.Empty;
        var usage = properties.TryGetValue("usage", out var usageValue) ? usageValue.GetString() : "tenant_api";

        var tenants = await _tenantService.ListAsync();

        return View(new OAuthClientFormViewModel
        {
            TenantId = Guid.TryParse(tenantId, out var parsed) ? parsed : null,
            Name = await _applicationManager.GetDisplayNameAsync(app) ?? string.Empty,
            ClientType = await _applicationManager.GetClientTypeAsync(app) ?? "public",
            Usage = string.IsNullOrWhiteSpace(usage) ? "tenant_api" : usage,
            RedirectUris = string.Join(",", redirectUris.Select(u => u.ToString())),
            Tenants = tenants
        });
    }

    [HttpPost("edit/{id}")]
    [AdminPermission(AdminPermissions.OAuthClientsEdit)]
    public async Task<IActionResult> Edit(string id, OAuthClientFormViewModel model)
    {
        if (!IsValidUsage(model.Usage))
        {
            ModelState.AddModelError(nameof(model.Usage), "Usage must be tenant_api, send_api, web_login, webhook_outbound, platform_service, or file_api.");
        }

        if (!IsTenantOptionalUsage(model.Usage) && (!model.TenantId.HasValue || model.TenantId.Value == Guid.Empty))
        {
            ModelState.AddModelError(nameof(model.TenantId), "Tenant is required for this usage.");
        }

        if (RequiresClientCredentials(model.Usage)
            && !string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
        {
            ModelState.AddModelError(nameof(model.ClientType), "Client type must be confidential for this usage.");
        }

        var redirectUris = NormalizeRedirectUris(model.RedirectUris, out var redirectUriError);
        if (!string.IsNullOrWhiteSpace(redirectUriError))
        {
            ModelState.AddModelError(nameof(model.RedirectUris), redirectUriError);
        }

        if (UsesAuthorizationCodeFlow(model.Usage) && redirectUris.Count == 0)
        {
            ModelState.AddModelError(nameof(model.RedirectUris), "Redirect URI is required for web_login or webhook_outbound usage.");
        }

        if (!ModelState.IsValid)
        {
            model.Tenants = await _tenantService.ListAsync();
            return View(model);
        }

        var app = await _applicationManager.FindByIdAsync(id);
        if (app is null)
        {
            return NotFound();
        }

        var descriptor = new OpenIddictApplicationDescriptor();
        await _applicationManager.PopulateAsync(descriptor, app);

        var originalClientType = await _applicationManager.GetClientTypeAsync(app);
        string? generatedClientSecret = null;

        descriptor.DisplayName = model.Name;
        descriptor.ClientType = model.ClientType;
        if (string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
        {
            descriptor.ClientSecret = null;
        }
        else if (string.Equals(model.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)
            && (!string.Equals(originalClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)
                || string.IsNullOrWhiteSpace(descriptor.ClientSecret)))
        {
            generatedClientSecret = GenerateClientSecret();
            descriptor.ClientSecret = generatedClientSecret;
        }

        await ApplyPermissionsAsync(descriptor, model.Usage);
        descriptor.RedirectUris.Clear();
        foreach (var uri in redirectUris)
        {
            descriptor.RedirectUris.Add(new Uri(uri));
        }

        if (!IsTenantOptionalUsage(model.Usage) && model.TenantId.HasValue)
        {
            descriptor.Properties["tenant_id"] = System.Text.Json.JsonSerializer.SerializeToElement(model.TenantId.Value.ToString());
        }
        else
        {
            descriptor.Properties.Remove("tenant_id");
        }

        descriptor.Properties["usage"] = System.Text.Json.JsonSerializer.SerializeToElement(model.Usage);

        await _applicationManager.UpdateAsync(app, descriptor);
        if (!string.IsNullOrWhiteSpace(generatedClientSecret))
        {
            TempData["GeneratedClientId"] = await _applicationManager.GetClientIdAsync(app);
            TempData["GeneratedClientSecret"] = generatedClientSecret;
        }

        return RedirectToAction("Index");
    }

    [HttpPost("delete/{id}")]
    [AdminPermission(AdminPermissions.OAuthClientsDelete)]
    public async Task<IActionResult> Delete(string id)
    {
        var app = await _applicationManager.FindByIdAsync(id);
        if (app is null)
        {
            return NotFound();
        }

        await _applicationManager.DeleteAsync(app);
        return RedirectToAction("Index");
    }

    [HttpPost("rotate-secret/{id}")]
    [AdminPermission(AdminPermissions.OAuthClientsRotateSecret)]
    public async Task<IActionResult> RotateSecret(string id)
    {
        var app = await _applicationManager.FindByIdAsync(id);
        if (app is null)
        {
            return NotFound();
        }

        var clientType = await _applicationManager.GetClientTypeAsync(app);
        if (!string.Equals(clientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
        {
            return BadRequest("Only confidential clients have secrets.");
        }

        var descriptor = new OpenIddictApplicationDescriptor();
        await _applicationManager.PopulateAsync(descriptor, app);

        var newSecret = GenerateClientSecret();
        descriptor.ClientSecret = newSecret;

        await _applicationManager.UpdateAsync(app, descriptor);

        TempData["RotatedClientId"] = await _applicationManager.GetClientIdAsync(app);
        TempData["RotatedClientSecret"] = newSecret;

        return RedirectToAction("Index");
    }

    private static bool IsValidUsage(string usage) =>
        usage is "tenant_api" or "send_api" or "web_login" or "webhook_outbound" or "platform_service" or "file_api";

    private static bool IsTenantOptionalUsage(string usage) =>
        usage is "platform_service" or "web_login";

    private static bool UsesAuthorizationCodeFlow(string usage) =>
        usage is "web_login" or "webhook_outbound";

    private static bool RequiresClientCredentials(string usage) =>
        usage is "tenant_api" or "send_api" or "platform_service" or "file_api";

    private static string GenerateClientSecret() =>
        Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32));

    private static List<string> NormalizeRedirectUris(string? value, out string? error)
    {
        error = null;
        var items = (value ?? string.Empty)
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .ToList();

        foreach (var item in items)
        {
            if (!Uri.TryCreate(item, UriKind.Absolute, out var uri)
                || (uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp))
            {
                error = "Redirect URIs must be valid absolute http/https URLs.";
                return new List<string>();
            }
        }

        return items;
    }

    private async Task<OpenIddictApplicationDescriptor> BuildDescriptorAsync(string clientId, string name, string clientType, string usage)
    {
        var descriptor = new OpenIddictApplicationDescriptor
        {
            ClientId = clientId,
            DisplayName = name,
            ClientType = clientType
        };

        await ApplyPermissionsAsync(descriptor, usage);
        return descriptor;
    }

    private async Task ApplyPermissionsAsync(OpenIddictApplicationDescriptor descriptor, string usage)
    {
        descriptor.Permissions.Clear();
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);

        if (usage is "web_login" or "webhook_outbound")
        {
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
        }
        else
        {
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
        }

        var scopes = await _authResourceRegistry.GetAllowedScopesForUsageAsync(usage, HttpContext.RequestAborted);
        foreach (var scope in scopes)
        {
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
        }
    }
}
