using MemberCenter.Api.Authorization;
using MemberCenter.Api.Contracts;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using System.Text.Json;

namespace MemberCenter.Api.Controllers;

[ApiController]
[Route("admin/oauth-clients")]
[Authorize(Policy = "Admin")]
[AdminPermission(AdminPermissions.OAuthClients)]
public class AdminOAuthClientsController : ControllerBase
{
    private readonly IOpenIddictApplicationManager _applicationManager;
    private readonly IAuthResourceRegistryService _authResourceRegistry;

    public AdminOAuthClientsController(
        IOpenIddictApplicationManager applicationManager,
        IAuthResourceRegistryService authResourceRegistry)
    {
        _applicationManager = applicationManager;
        _authResourceRegistry = authResourceRegistry;
    }

    [HttpGet]
    [AdminPermission(AdminPermissions.OAuthClientsIndex)]
    public async Task<IActionResult> List()
    {
        var results = new List<object>();
        await foreach (var application in _applicationManager.ListAsync())
        {
            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),
                redirect_uris = await _applicationManager.GetRedirectUrisAsync(application),
                properties = await _applicationManager.GetPropertiesAsync(application)
            });
        }

        return Ok(results);
    }

    [HttpPost]
    [AdminPermission(AdminPermissions.OAuthClientsCreate)]
    public async Task<IActionResult> Create([FromBody] OAuthClientRequest request)
    {
        if (!IsValidUsage(request.Usage))
        {
            return BadRequest("usage must be tenant_api, send_api, web_login, webhook_outbound, platform_service, or file_api.");
        }

        if (!IsTenantOptionalUsage(request.Usage) && (!request.TenantId.HasValue || request.TenantId.Value == Guid.Empty))
        {
            return BadRequest("tenant_id is required for this usage.");
        }

        if (RequiresClientCredentials(request.Usage)
            && !string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
        {
            return BadRequest("client_type must be confidential for this usage.");
        }

        var (redirectUris, redirectUriError) = NormalizeRedirectUris(request.RedirectUris);
        if (!string.IsNullOrWhiteSpace(redirectUriError))
        {
            return BadRequest(redirectUriError);
        }
        if (UsesAuthorizationCodeFlow(request.Usage) && redirectUris.Count == 0)
        {
            return BadRequest("redirect_uris is required for web_login or webhook_outbound usage.");
        }

        var descriptor = new OpenIddictApplicationDescriptor
        {
            ClientId = Guid.NewGuid().ToString("N"),
            DisplayName = request.Name,
            ClientType = request.ClientType
        };
        var clientSecret = string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)
            ? GenerateClientSecret()
            : null;
        if (!string.IsNullOrWhiteSpace(clientSecret))
        {
            descriptor.ClientSecret = clientSecret;
        }

        await ApplyPermissionsAsync(descriptor, request.Usage);

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

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

        await _applicationManager.CreateAsync(descriptor);

        return Created("/admin/oauth-clients", new
        {
            descriptor.ClientId,
            descriptor.DisplayName,
            descriptor.ClientType,
            client_secret = clientSecret,
            redirect_uris = descriptor.RedirectUris.Select(u => u.ToString())
        });
    }

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

        return Ok(new
        {
            id,
            name = await _applicationManager.GetDisplayNameAsync(app),
            client_id = await _applicationManager.GetClientIdAsync(app),
            client_type = await _applicationManager.GetClientTypeAsync(app),
            redirect_uris = await _applicationManager.GetRedirectUrisAsync(app),
            properties = await _applicationManager.GetPropertiesAsync(app)
        });
    }

    [HttpPut("{id}")]
    [AdminPermission(AdminPermissions.OAuthClientsEdit)]
    public async Task<IActionResult> Update(string id, [FromBody] OAuthClientRequest request)
    {
        if (!IsValidUsage(request.Usage))
        {
            return BadRequest("usage must be tenant_api, send_api, web_login, webhook_outbound, platform_service, or file_api.");
        }

        if (!IsTenantOptionalUsage(request.Usage) && (!request.TenantId.HasValue || request.TenantId.Value == Guid.Empty))
        {
            return BadRequest("tenant_id is required for this usage.");
        }

        if (RequiresClientCredentials(request.Usage)
            && !string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase))
        {
            return BadRequest("client_type must be confidential for this usage.");
        }

        var (redirectUris, redirectUriError) = NormalizeRedirectUris(request.RedirectUris);
        if (!string.IsNullOrWhiteSpace(redirectUriError))
        {
            return BadRequest(redirectUriError);
        }
        if (UsesAuthorizationCodeFlow(request.Usage) && redirectUris.Count == 0)
        {
            return BadRequest("redirect_uris is required for web_login or webhook_outbound usage.");
        }

        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 = request.Name;
        descriptor.ClientType = request.ClientType;
        if (string.Equals(request.ClientType, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
        {
            descriptor.ClientSecret = null;
        }
        else if (string.Equals(request.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, request.Usage);
        descriptor.RedirectUris.Clear();
        foreach (var uri in redirectUris)
        {
            descriptor.RedirectUris.Add(new Uri(uri));
        }
        if (!IsTenantOptionalUsage(request.Usage) && request.TenantId.HasValue)
        {
            descriptor.Properties["tenant_id"] = JsonSerializer.SerializeToElement(request.TenantId.Value.ToString());
        }
        else
        {
            descriptor.Properties.Remove("tenant_id");
        }
        descriptor.Properties["usage"] = JsonSerializer.SerializeToElement(request.Usage);

        await _applicationManager.UpdateAsync(app, descriptor);

        return Ok(new
        {
            id,
            descriptor.DisplayName,
            descriptor.ClientType,
            client_secret = generatedClientSecret,
            redirect_uris = descriptor.RedirectUris.Select(u => u.ToString())
        });
    }

    [HttpDelete("{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 NoContent();
    }

    private static bool IsValidUsage(string usage)
    {
        return string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "send_api", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "file_api", StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsTenantOptionalUsage(string usage)
    {
        return string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase);
    }

    private static bool RequiresClientCredentials(string usage)
    {
        return string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "send_api", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "file_api", StringComparison.OrdinalIgnoreCase);
    }

    private static bool UsesAuthorizationCodeFlow(string usage)
    {
        return string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase);
    }

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

    private static (List<string> Uris, string? Error) NormalizeRedirectUris(List<string>? redirectUris)
    {
        if (redirectUris is null || redirectUris.Count == 0)
        {
            return ([], null);
        }

        var values = redirectUris
            .Where(uri => !string.IsNullOrWhiteSpace(uri))
            .Select(uri => uri.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();

        foreach (var uri in values)
        {
            if (!Uri.TryCreate(uri, UriKind.Absolute, out _))
            {
                return ([], "redirect_uris must contain absolute URIs only.");
            }
        }

        return (values, null);
    }

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

        if (UsesAuthorizationCodeFlow(usage))
        {
            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);
        }
    }
}
