using System.Net;
using System.Security.Authentication;
using Autofac;
using FitConnect.Encryption;
using FitConnect.Models;
using FitConnect.Models.v1.Api;
using FitConnect.Services;
using FitConnect.Services.Interfaces;
using FitConnect.Services.Models.v1.Submission;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using NJsonSchema;
using NJsonSchema.Validation;
using SecurityEventToken = FitConnect.Models.SecurityEventToken;

namespace FitConnect;

public abstract class FitConnectClient {
    public const string GuidPattern =
        "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";

    public const string LeikaKeyPattern =
        "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$";

    private readonly string? _privateKeyDecryption;
    private readonly string? _privateKeySigning;
    private readonly string? _publicKeyEncryption;
    private readonly string? _publicKeySignatureVerification;
    protected readonly bool VerifiedCertificatesAreMandatory;

    protected FitConnectClient(FitConnectEnvironment environment,
        string clientId, string clientSecret,
        ILogger? logger = null,
        string? privateKeyDecryption = null,
        string? privateKeySigning = null,
        string? publicKeyEncryption = null,
        string? publicKeySignatureVerification = null
    ) : this() {
        _privateKeyDecryption = privateKeyDecryption;
        _privateKeySigning = privateKeySigning;
        _publicKeyEncryption = publicKeyEncryption;
        _publicKeySignatureVerification = publicKeySignatureVerification;
        OAuthService = new OAuthService(environment.TokenUrl, "V1", clientId, clientSecret, logger);
        SubmissionService =
            new SubmissionService(environment.SubmissionUrl[0], OAuthService,
                publicKeySignatureVerification == null
                    ? null
                    : new JsonWebKey(publicKeySignatureVerification),
                logger: logger);
        RouteService = new RouteService(environment.RoutingUrl, logger: logger);
        CasesService = new CasesService(environment.SubmissionUrl[0], OAuthService, logger: logger);
        DestinationService =
            new DestinationService(environment.SubmissionUrl[0], OAuthService, logger: logger);
        Logger = logger;
        VerifiedCertificatesAreMandatory = environment.VerifiedCertificatesAreMandatory;
    }

    protected FitConnectClient(FitConnectEnvironment environment,
        string clientId, string clientSecret,
        IContainer container,
        string? privateKeyDecryption = null,
        string? privateKeySigning = null,
        string? publicKeyEncryption = null,
        string? publicKeySignatureVerification = null
    ) : this() {
        _privateKeyDecryption = privateKeyDecryption;
        _privateKeySigning = privateKeySigning;
        _publicKeyEncryption = publicKeyEncryption;
        _publicKeySignatureVerification = publicKeySignatureVerification;
        OAuthService = container.Resolve<IOAuthService>();
        SubmissionService = container.Resolve<ISubmissionService>();
        RouteService = container.Resolve<IRouteService>();
        CasesService = container.Resolve<ICasesService>();
        DestinationService = container.Resolve<IDestinationService>();
        Logger = container.Resolve<ILogger>();
    }


    private FitConnectClient() {
        // Take care of OAuthService if make the constructor public
        Encryption = new FitEncryption(Logger);
    }


    public IOAuthService OAuthService { get; } =
        null!; // To resolve the warning, is only applicable for the **private** constructor

    protected ISubmissionService SubmissionService { get; } = null!;
    protected IRouteService RouteService { get; } = null!;
    protected IDestinationService DestinationService { get; } = null!;
    protected ICasesService CasesService { get; } = null!;
    protected ILogger? Logger { get; }
    protected FitEncryption Encryption { get; set; }

    /// <summary>
    ///     Validates <paramref name="dataString" /> against <paramref name="schemaString" /> and returns
    ///     the validation errors if any appear
    /// </summary>
    public static async Task<ICollection<ValidationError>> ValidateSchemaAsync(
        string dataString, string schemaString) {
        var schema = await JsonSchema.FromJsonAsync(schemaString);
        return schema.Validate(dataString);
    }

    public void SetProxy(WebProxy proxy) {
        OAuthService.Proxy = proxy;
        SubmissionService.Proxy = proxy;
        DestinationService.Proxy = proxy;
        RouteService.Proxy = proxy;
    }

    public WebProxy? GetProxy() {
        return OAuthService.Proxy;
    }


    /// <summary>
    ///     Retrieve the events for the submission
    /// </summary>
    /// <param name="caseId"></param>
    /// <param name="destinationId"></param>
    /// <param name="skipTest"></param>
    /// <returns></returns>
    public async Task<List<SecurityEventToken>> GetEventLogAsync(string caseId,
        string destinationId,
        bool skipTest = false) {
        var events =
            (await SubmissionService.GetEventLogAsync(caseId, destinationId, skipTest))
            ?.Select(SecurityEventToken.FromJwtEncodedString).ToList() ??
            new List<SecurityEventToken>();
        return events;
    }

    /// <summary>
    ///     Retrieve the events for the submission
    /// </summary>
    /// <param name="submission"></param>
    /// <param name="skipTest"></param>
    /// <returns></returns>
    public async Task<List<SecurityEventToken>> GetEventLogAsync(
        SubmissionForPickupDto submission,
        bool skipTest = false) {
        if (submission?.CaseId == null || submission.DestinationId == null)
            throw new ArgumentNullException(nameof(submission));

        var events = await SubmissionService
            .GetEventLogAsync(submission.CaseId, submission.DestinationId, skipTest);
        return events?.Select(SecurityEventToken.FromJwtEncodedString).ToList() ??
               new List<SecurityEventToken>();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="submission"></param>
    /// <returns></returns>
    public async Task<SubmissionState> GetStatusForSubmissionAsync(
        SentSubmission submission) {
        var eventLog = await GetEventLogAsync(submission.CaseId, submission.DestinationId);

        var status = eventLog.Last(e => e.SubmissionId == submission.SubmissionId);
        List<Problems>? problems = null;

        if (status.Events?.AcceptSubmissionEvent != null) {
            if (!(status.Events.AcceptSubmissionEvent.AuthenticationTags ??
                  throw new InvalidOperationException()).AreEqualTo(submission
                    .AuthenticationTags)) {
                throw new AuthenticationTagException("Authentication tags are not equal");
            }

            problems = status.Events.AcceptSubmissionEvent.Problems;
        }
        else if (status.Events?.RejectSubmissionEvent != null)
            problems = status.Events.RejectSubmissionEvent.Problems;

        return new SubmissionState(status.EventType, problems);
    }

    /// <summary>
    ///     Encrypt attachments (Anhänge)
    /// </summary>
    /// <param name="publicKey"></param>
    /// <param name="attachment"></param>
    /// <returns></returns>
    public KeyValuePair<string, string> Encrypt(string publicKey, Attachment attachment) {
        var content = Encryption.Encrypt(attachment.Content ?? Array.Empty<byte>(), publicKey);
        return new KeyValuePair<string, string>(attachment.Id, content);
    }

    /// <summary>
    ///     Encrypt attachments (Anhänge)
    /// </summary>
    /// <param name="publicKey">Public key for encryption</param>
    /// <param name="attachments">List of attachments to encrypt</param>
    /// <returns></returns>
    public Dictionary<string, string> Encrypt(string publicKey,
        IEnumerable<Attachment> attachments) {
        return attachments.Select(a => Encrypt(publicKey, a))
            .ToDictionary(p => p.Key, p => p.Value);
    }
}

public static class FitConnectClientExtensions {
    /// <summary>
    /// </summary>
    /// <param name="host"></param>
    /// <param name="port"></param>
    /// <param name="username"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    public static T WithProxy<T>(this T caller, string host, int port, string? username = null,
        string? password = null) where T : FitConnectClient {
        var proxy = new WebProxy(host, port);
        if (username != null && password != null)
            proxy.Credentials = new NetworkCredential(username, password);

        caller.SetProxy(proxy);
        return caller;
    }
}