using MemberCenter.Api.Contracts;
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")]
public class AdminOAuthClientsController : ControllerBase
{
    private readonly IOpenIddictApplicationManager _applicationManager;

    public AdminOAuthClientsController(IOpenIddictApplicationManager applicationManager)
    {
        _applicationManager = applicationManager;
    }

    [HttpGet]
    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]
    public async Task<IActionResult> Create([FromBody] OAuthClientRequest request)
    {
        if (!IsValidUsage(request.Usage))
        {
            return BadRequest("usage must be tenant_api, send_api, webhook_outbound, or platform_service.");
        }

        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 webhook_outbound usage.");
        }

        var descriptor = new OpenIddictApplicationDescriptor
        {
            ClientId = Guid.NewGuid().ToString("N"),
            DisplayName = request.Name,
            ClientType = request.ClientType
        };
        ApplyPermissions(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,
            redirect_uris = descriptor.RedirectUris.Select(u => u.ToString())
        });
    }

    [HttpGet("{id}")]
    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}")]
    public async Task<IActionResult> Update(string id, [FromBody] OAuthClientRequest request)
    {
        if (!IsValidUsage(request.Usage))
        {
            return BadRequest("usage must be tenant_api, send_api, webhook_outbound, or platform_service.");
        }

        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 webhook_outbound usage.");
        }

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

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

        descriptor.DisplayName = request.Name;
        descriptor.ClientType = request.ClientType;
        ApplyPermissions(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,
            redirect_uris = descriptor.RedirectUris.Select(u => u.ToString())
        });
    }

    [HttpDelete("{id}")]
    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, "webhook_outbound", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsTenantOptionalUsage(string usage)
    {
        return string.Equals(usage, "platform_service", 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);
    }

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

    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 static void ApplyPermissions(OpenIddictApplicationDescriptor descriptor, string usage)
    {
        descriptor.Permissions.Clear();
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);

        if (string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "tenant_api", StringComparison.OrdinalIgnoreCase)
            || string.Equals(usage, "send_api", StringComparison.OrdinalIgnoreCase))
        {
            descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
            if (string.Equals(usage, "platform_service", StringComparison.OrdinalIgnoreCase))
            {
                descriptor.Permissions.Add("scp:newsletter:events.write.global");
            }
            else if (string.Equals(usage, "send_api", StringComparison.OrdinalIgnoreCase))
            {
                descriptor.Permissions.Add("scp:newsletter:send.write");
                descriptor.Permissions.Add("scp:newsletter:send.read");
            }
            else
            {
                descriptor.Permissions.Add("scp:newsletter:events.write");
            }
            return;
        }

        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);
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.Scopes.Email);
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.Scopes.Profile);
        descriptor.Permissions.Add("scp:openid");
    }
}
