using Hl7.Fhir.Rest;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Pgo.Backend.Config;
using Pgo.Backend.Interfaces;
using Pgo.Backend.Models;
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Net;
using System.Web;
using System.Text.Json;
using Hl7.Fhir.Model;
using Task = System.Threading.Tasks.Task;
using System.Net.Mail;
using System;
using Attachment = Hl7.Fhir.Model.Attachment;
using Pgo.Backend.Services;
using static Grpc.Core.Metadata;

namespace Pgo.Backend.Controllers;

[ApiController]
public class ImportController(ILogger<ImportController> logger, IHttpClientFactory httpClientFactory, IOptions<PgoConfig> configuration, IStelselNodeService stelselnode, SimpleDownloadStatusNotificationService notificationService) : ControllerBase
{
    private readonly ILogger<ImportController> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    private readonly PgoConfig _config = configuration.Value ?? throw new ArgumentNullException(nameof(configuration));
    private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
    private readonly IStelselNodeService _stelselnode = stelselnode ?? throw new ArgumentNullException(nameof(stelselnode));
    private readonly SimpleDownloadStatusNotificationService _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));

    // Some static frontend urls to simplify demo, alternatively push a return-url through the flow.
    private string _frontendSuccessUrl => $"{_config.BasePath}/bekijken";
    private string _frontendCancelledUrl => $"{_config.BasePath}/bekijken/annuleren";


    /// <summary>
    /// Start remote authorization and consent flow by redirecting user to the APD's or DVA's authorization endpoint.
    /// We'll receive a code in the callback when the user returns.
    /// </summary>
    [HttpGet("api/import/login/{provider}")]
    public IActionResult RemoteLogin(string? provider)
    {
        var remotePgo = _stelselnode
            .GetZorgaanbieders()
            .Where(provider => provider.ProviderType == StelselNodeProviderType.AanbiederPersoonsdomein)
            .FirstOrDefault(adp => adp?.Name == provider);

        if (remotePgo == null)
        {
            _logger.LogError($"Login request failed, no provider '{provider}' found matching APD.");
            return Redirect(_frontendCancelledUrl + "?error=" + HttpUtility.UrlEncode($"Aanbieder {provider} niet gevonden."));
        }

        // Redirect the user to authorization endpoint, include a redirect_uri pointing back to our callback.
        // Generate a state to prevent CSRF attacks, include scope 51 for consent to document exchange, etc.
        var requestId = Guid.NewGuid().ToString("N");
        var correlationId = Guid.NewGuid().ToString("N");
        var scope = HttpUtility.UrlEncode(_config.RemoteIdentScopes); // 51
        var redirectUri = HttpUtility.UrlEncode(_config.RemoteIdentRedirectUri); // api/import/login_callback
        var state = LoginState.Create(remotePgo.Name);
        var endpoint = $"{remotePgo.AuthorizationEndpoint}?client_id={_config.RemoteIdentClientId}&redirect_uri={redirectUri}&response_type=code&scope={scope}&state={state}&MedMij-Request-ID={requestId}&X-Correlation-ID={correlationId}";
        HttpContext.Session.SetString("oauth_state", state);

        return Redirect(endpoint);
    }

    /// <summary>
    /// After visiting remote authorization endpoint, the user is redirected back with a code or an error.
    /// We exchange the code for a token and store it in user session to exchange documents with remote ADP.
    /// </summary>
    /// <returns></returns>
    [HttpGet("api/import/login_callback")]
    public async Task<IActionResult> RemoteLoginCallback([FromQuery] string? code, [FromQuery] string? state, [FromQuery] string? scope, [FromQuery] string? error, [FromQuery] string? error_description)
    {
        // Sanity checks. Is this the same user as the one that started the flow?
        var loginState = LoginState.Parse(state);
        var sessionState = HttpContext.Session.GetString("oauth_state");                
        if (string.IsNullOrEmpty(state) || sessionState != state)
        {
            _logger.LogError("User returning from authorization endpoint with invalid session state.");
            return Redirect(_frontendCancelledUrl + "?error=" + HttpUtility.UrlEncode($"Aanbieder stuurde een onjuiste session state mee."));
        }

        // Is there an error message in the callback?
        if (!string.IsNullOrWhiteSpace(error))
        {
            _logger.LogError("Authorization request failed: {0}", error);
            return Redirect(_frontendCancelledUrl + "?error=" + HttpUtility.UrlEncode($"Aanbieder {loginState.ProviderName} gaf geen code mee, maar toonde deze foutmelding: {error} {error_description}"));
        }

        // Fetch the token exchange endpoint from ZAL for remote PGO in this session.
        var remotePgo = _stelselnode
            .GetZorgaanbieders()
            .Where(provider => provider.ProviderType == StelselNodeProviderType.AanbiederPersoonsdomein)
            .FirstOrDefault(adp => adp?.Name == loginState.ProviderName);

        if (remotePgo == null)
        {
            _logger.LogError($"Login request failed, no provider '{loginState.ProviderName}' found matching APD.");
            return Redirect(_frontendCancelledUrl + "?error=" + HttpUtility.UrlEncode($"Aanbieder {loginState.ProviderName} niet gevonden."));
        }

        // Exchange the authorization_code for an access token and store that in user session.
        try
        {
            var httpClient = _httpClientFactory.CreateClient();
            var tokenResponse = await httpClient.PostAsync(remotePgo.TokenEndpoint, new FormUrlEncodedContent(
                new Dictionary<string, string>
                {
                    { "grant_type", "authorization_code" },
                    { "code", code },
                    { "redirect_uri", _config.RemoteIdentRedirectUri },
                    { "client_id", _config.RemoteIdentClientId },
                }
            ));

            if (!tokenResponse.IsSuccessStatusCode)
            {
                var tokenResponseError = await tokenResponse.Content.ReadAsStringAsync();
                throw new HttpRequestException($"HTTP {tokenResponse.StatusCode}, {tokenResponseError}");
            }
            else
            {
                var token = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();
                if (token == null)
                {
                    throw new JsonException("No token message found in body of succesful token response.");
                }
                HttpContext.Session.SetString("access_token", token.AccessToken);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError("Token request failed for {0}: {1}", remotePgo.TokenEndpoint, ex.Message);
            return Redirect(_frontendCancelledUrl + "?error=" + HttpUtility.UrlEncode($"Aanbieder {loginState.ProviderName} gaf een code mee, maar liet deze niet inwisselen voor een token."));
        }

        return Redirect($"{_config.BasePath}/api/import/download-documents");
    }



    /// <summary>
    /// Fetch a bundle of FHIR DocumentReferences for user and retrieve linked PDF/A documents.
    /// Some documents may be stored as inline data, others are referenced by content.attachment.url.
    /// </summary>
    [HttpGet("api/import/download-documents")]
    public async Task<ActionResult> DownloadDocumentsBundle()
    {
        // DEMO: Sanity check.
        var userId = User.FindFirst("sub")?.Value ?? "voorbeeldgebruiker";
        var sessionState = LoginState.Parse(HttpContext.Session.GetString("oauth_state"));
        var accessToken = HttpContext.Session.GetString("access_token");
        var remotePgo = _stelselnode
            .GetZorgaanbieders()
            .Where(provider => provider.ProviderType == StelselNodeProviderType.AanbiederPersoonsdomein)
            .FirstOrDefault(adp => adp?.Name == sessionState.ProviderName);

        if (sessionState == null || accessToken == null || remotePgo == null)
        {
            _logger.LogError($"Download of DocumentReferences bundle failed, could not restore state or access token.");
            return Redirect(_frontendCancelledUrl + "?error=" + HttpUtility.UrlEncode($"Tijdens het downloaden ontbrak toegangsinformatie."));
        }

        // DEMO: Fetch bundle of FHIR DocumentReferences resources.
        var fhirClient = CreateFhirClient(remotePgo.FhirResourceEndpoint, accessToken);
        var bundle = await fhirClient.SearchAsync<DocumentReference>(new SearchParams().Where("type=http://loinc.org|34133-9"));
        foreach (var entry in bundle?.Entry ?? [])
        {
            switch (entry.Resource.TypeName)
            {
                case "Patient":
                    // In this proof-of-concept we don't process structured data for the patient as the aim is to simply fetch documents.
                    // PGO may benefit though from processing the patient record that is passed or referenced by DocumentReference.subject.
                    var patient = entry.Resource as Patient;
                    break;

                case "DocumentReference":
                    // Read the document attachment and store it for the user in our local FHIR server with remote APD as source system.
                    // For remote servers it may take a long time to generate the document. So, we download documents in the background.
                    var documentReference = entry.Resource as DocumentReference;
                    foreach (var content in documentReference?.Content ?? [])
                    {
                        if (content == null) continue;

                        // Store inline attachment.data or fetch attachment.url.
                        var contentType = content.Attachment?.ContentType ?? throw new ArgumentNullException("ContentType");
                        var hasInlineData = content.Attachment?.Data != null && content.Attachment?.Data != Array.Empty<byte>();
                        if (hasInlineData)
                        {
                            await StoreDocument(userId, remotePgo.Name, content.Attachment);
                        }
                        else
                        {
                            // This is a simple example to push work to background while we redirect the user back to frontend.
                            // Helpful as concept, more appropriate to use an IHostedService, BackgroundService, HangFire, etc.
                            var contentUrl = content.Attachment?.Url ?? "";
                            var contentUrlFullyQualified = Url.IsLocalUrl(contentUrl) ? $"{fhirClient.Endpoint.Scheme}://{fhirClient.Endpoint.Host}:{fhirClient.Endpoint.Port}{contentUrl}" : contentUrl;
                            _ = Task.Run(async () =>
                            {
                                try
                                {
                                    _notificationService.UpdateStatus(userId, isDownloading: true);
                                    var data = await DownloadFromUrl(contentUrlFullyQualified);
                                    await StoreDocument(userId, remotePgo.Name, content.Attachment, data);
                                }
                                catch (Exception ex)
                                {
                                    _logger.LogError("Download of document failed: {0}", ex.Message);
                                }
                                finally
                                {
                                    _notificationService.UpdateStatus(userId, isDownloading: false);
                                }
                            });
                        }
                    }
                    break;

                default:
                    break;
            }
        }

        return Redirect(_frontendSuccessUrl);
    }

    /// <summary>
    /// Returns a status for defered downloads after Asynchrounous Response (302 Accepted) message from source.
    /// Usually, a download is processed with a minute or so, after which the document is available in local FHIR resource server.
    /// </summary>
    /// <returns></returns>
    [HttpGet("api/import/status")]
    public IActionResult DownloadStatus()
    {
        var count = _notificationService.Status.Where(status => status.Value.IsDownloading).Count();

        return Ok(new { DownloadsInProgress = count });
    }

    /// <summary>
    /// A simple reset endpoint to take out downloads in progress and earlier imported resources for demonstration purpose.
    /// </summary>
    [HttpGet("api/import/reset")]
    public async Task<IActionResult> DownloadReset()
    {
        _notificationService.Status.Clear();
        var fhirClient = CreateFhirClient(_config.FhirEndpoint);
        //await fhirClient.DeleteAsync("DocumentReference", new SearchParams().Where("type=http://loinc.org|34133-9"));
        //return Ok(0);

        var bundle = await fhirClient.SearchAsync<DocumentReference>(new SearchParams().Where("type=http://loinc.org|34133-9"));
        foreach (var entry in bundle?.Entry ?? [])
        {
            await fhirClient.DeleteAsync(entry.Resource);
        }
        return Ok(bundle?.Entry.Count() ?? 0);
    }

    private FhirClient CreateFhirClient(string fhirBaseEndpoint, string? accessToken = null)
    {
        var httpClient = _httpClientFactory.CreateClient("FhirClient");
        httpClient.DefaultRequestHeaders.Authorization = string.IsNullOrEmpty(accessToken) ? null : new AuthenticationHeaderValue("Bearer", accessToken);

        var settings = new FhirClientSettings
        {
            PreferredFormat = ResourceFormat.Json,
            VerifyFhirVersion = false,
            PreferredParameterHandling = SearchParameterHandling.Strict
        };
        return new FhirClient(fhirBaseEndpoint, httpClient, settings);
    }

    /// <summary>
    /// Store a document in our local FHIR server. We create a new DocumentReference for our own administration
    /// and copy some of the metadata from the original referenced attachment. Available inline data is copied too.
    /// </summary>
    private async Task StoreDocument(string userPatientId, string provider, Attachment attachment, byte[]? dataImportedFromAttachmentUrl = null)
    {
        var fhirClient = CreateFhirClient(_config.FhirEndpoint);

        var resource = new DocumentReference
        {
            Status = DocumentReferenceStatus.Current,
            Identifier = new List<Identifier> { new Identifier("https://pgo-doel/bronsystemen", provider) },
            Type = new CodeableConcept("http://loinc.org", "34133-9", "Samenvatting medische gegevens uit ander bronsysteeem"),
            Subject = new ResourceReference($"Patient/{userPatientId}"),
            Indexed = DateTimeOffset.UtcNow,
            Content = new()
            {
                new()
                {
                    Attachment = new()
                    {
                        ContentType = attachment.ContentType ?? throw new ArgumentNullException("ContentType"),
                        Data = dataImportedFromAttachmentUrl ?? attachment.Data,
                        Title = attachment.Title ?? $"Document van {provider}",
                    }
                }
            }
        };

        await fhirClient.CreateAsync(resource);
    }

    /// <summary>
    /// Download a document attachment from url. The remote server may return a 202 Accepted instead of 200 OK
    /// if generating the document takes time. We poll the status endpoint until the document is ready.
    /// </summary>
    private async Task<byte[]> DownloadFromUrl(string url)
    {
        //if (Debugger.IsAttached)
        //{
        //    // Quick override for local testing.
        //    url = "https://localhost:5001/api/resources/genereer_dossier_pdf";
        //}

        var httpclient = _httpClientFactory.CreateClient("WebClient");
        var originalUri = new Uri(url);
        var timeoutAfter = DateTime.UtcNow.AddSeconds(5 * 60);

        while (true)
        {
            if (DateTime.UtcNow > timeoutAfter)
            {
                throw new TimeoutException($"Timeout while waiting for document at {url}");
            }

            HttpResponseMessage response = await httpclient.GetAsync(url);
            switch (response.StatusCode)
            {
                // HTTP 200 OK at api/resources/genereer_dossier_pdf
                case HttpStatusCode.OK:
                    return await response.Content.ReadAsByteArrayAsync();

                // HTTP 202 Accepted, Location: /api/resources/status/123
                // HTTP 200 Accepted, RetryAfter: 2
                case HttpStatusCode.Accepted:
                case HttpStatusCode.TooManyRequests:
                case HttpStatusCode.ServiceUnavailable:
                    if (response.Headers.Location != null)
                    {
                        if (Url.IsLocalUrl(response.Headers.Location.ToString()))
                        {
                            url = new Uri(originalUri, response.Headers.Location).ToString();
                        }
                        else
                        {
                            url = response.Headers.Location.ToString();
                        }
                    }
                    int retryAfter = response.Headers.RetryAfter == null ? 5 : response.Headers.RetryAfter.Delta?.Seconds ?? 5;
                    await Task.Delay(TimeSpan.FromSeconds(retryAfter));
                    break;

                default:
                    throw new HttpRequestException($"Unexpected response in {nameof(DownloadFromUrl)}: {response.StatusCode}");
            }
        }
    }
}
