using System;
using System.Collections.Generic;
using Npgsql.Internal;
using Npgsql.Logging;
using Npgsql.Util;

namespace Npgsql.BackendMessages;

abstract class AuthenticationRequestMessage : IBackendMessage
{
    public BackendMessageCode Code => BackendMessageCode.AuthenticationRequest;
    internal abstract AuthenticationRequestType AuthRequestType { get; }
}

class AuthenticationOkMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationOk;

    internal static readonly AuthenticationOkMessage Instance = new();
    AuthenticationOkMessage() { }
}

class AuthenticationKerberosV5Message : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationKerberosV5;

    internal static readonly AuthenticationKerberosV5Message Instance = new();
    AuthenticationKerberosV5Message() { }
}

class AuthenticationCleartextPasswordMessage  : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationCleartextPassword;

    internal static readonly AuthenticationCleartextPasswordMessage Instance = new();
    AuthenticationCleartextPasswordMessage() { }
}

class AuthenticationMD5PasswordMessage  : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationMD5Password;

    internal byte[] Salt { get; private set; }

    internal static AuthenticationMD5PasswordMessage Load(NpgsqlReadBuffer buf)
    {
        var salt = new byte[4];
        buf.ReadBytes(salt, 0, 4);
        return new AuthenticationMD5PasswordMessage(salt);
    }

    AuthenticationMD5PasswordMessage(byte[] salt)
    {
        Salt = salt;
    }
}

class AuthenticationSCMCredentialMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationSCMCredential;

    internal static readonly AuthenticationSCMCredentialMessage Instance = new();
    AuthenticationSCMCredentialMessage() { }
}

class AuthenticationGSSMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationGSS;

    internal static readonly AuthenticationGSSMessage Instance = new();
    AuthenticationGSSMessage() { }
}

class AuthenticationGSSContinueMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationGSSContinue;

    internal byte[] AuthenticationData { get; private set; }

    internal static AuthenticationGSSContinueMessage Load(NpgsqlReadBuffer buf, int len)
    {
        len -= 4;   // The AuthRequestType code
        var authenticationData = new byte[len];
        buf.ReadBytes(authenticationData, 0, len);
        return new AuthenticationGSSContinueMessage(authenticationData);
    }

    AuthenticationGSSContinueMessage(byte[] authenticationData)
    {
        AuthenticationData = authenticationData;
    }
}

class AuthenticationSSPIMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationSSPI;

    internal static readonly AuthenticationSSPIMessage Instance = new();
    AuthenticationSSPIMessage() { }
}

#region SASL

class AuthenticationSASLMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationSASL;
    internal List<string> Mechanisms { get; } = new();

    internal AuthenticationSASLMessage(NpgsqlReadBuffer buf)
    {
        while (buf.Buffer[buf.ReadPosition] != 0)
            Mechanisms.Add(buf.ReadNullTerminatedString());
        buf.ReadByte();
        if (Mechanisms.Count == 0)
            throw new NpgsqlException("Received AuthenticationSASL message with 0 mechanisms!");
    }
}

class AuthenticationSASLContinueMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationSASLContinue;
    internal byte[] Payload { get; }

    internal AuthenticationSASLContinueMessage(NpgsqlReadBuffer buf, int len)
    {
        Payload = new byte[len];
        buf.ReadBytes(Payload, 0, len);
    }
}

class AuthenticationSCRAMServerFirstMessage
{
    static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(AuthenticationSCRAMServerFirstMessage));

    internal string Nonce { get; }
    internal string Salt { get; }
    internal int Iteration { get; }

    internal static AuthenticationSCRAMServerFirstMessage Load(byte[] bytes)
    {
        var data = PGUtil.UTF8Encoding.GetString(bytes);
        string? nonce = null, salt = null;
        var iteration = -1;

        foreach (var part in data.Split(','))
        {
            if (part.StartsWith("r=", StringComparison.Ordinal))
                nonce = part.Substring(2);
            else if (part.StartsWith("s=", StringComparison.Ordinal))
                salt = part.Substring(2);
            else if (part.StartsWith("i=", StringComparison.Ordinal))
                iteration = int.Parse(part.Substring(2));
            else
                Log.Debug("Unknown part in SCRAM server-first message:" + part);
        }

        if (nonce == null)
            throw new NpgsqlException("Server nonce not received in SCRAM server-first message");
        if (salt == null)
            throw new NpgsqlException("Server salt not received in SCRAM server-first message");
        if (iteration == -1)
            throw new NpgsqlException("Server iterations not received in SCRAM server-first message");

        return new AuthenticationSCRAMServerFirstMessage(nonce, salt, iteration);
    }

    AuthenticationSCRAMServerFirstMessage(string nonce, string salt, int iteration)
    {
        Nonce = nonce;
        Salt = salt;
        Iteration = iteration;
    }
}

class AuthenticationSASLFinalMessage : AuthenticationRequestMessage
{
    internal override AuthenticationRequestType AuthRequestType => AuthenticationRequestType.AuthenticationSASLFinal;
    internal byte[] Payload { get; }

    internal AuthenticationSASLFinalMessage(NpgsqlReadBuffer buf, int len)
    {
        Payload = new byte[len];
        buf.ReadBytes(Payload, 0, len);
    }
}

class AuthenticationSCRAMServerFinalMessage
{
    static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(AuthenticationSCRAMServerFinalMessage));

    internal string ServerSignature { get; }

    internal static AuthenticationSCRAMServerFinalMessage Load(byte[] bytes)
    {
        var data = PGUtil.UTF8Encoding.GetString(bytes);
        string? serverSignature = null;

        foreach (var part in data.Split(','))
        {
            if (part.StartsWith("v=", StringComparison.Ordinal))
                serverSignature = part.Substring(2);
            else
                Log.Debug("Unknown part in SCRAM server-first message:" + part);
        }

        if (serverSignature == null)
            throw new NpgsqlException("Server signature not received in SCRAM server-final message");

        return new AuthenticationSCRAMServerFinalMessage(serverSignature);
    }

    internal AuthenticationSCRAMServerFinalMessage(string serverSignature)
        => ServerSignature = serverSignature;
}

#endregion SASL

// TODO: Remove Authentication prefix from everything
enum AuthenticationRequestType
{
    AuthenticationOk = 0,
    AuthenticationKerberosV4 = 1,
    AuthenticationKerberosV5 = 2,
    AuthenticationCleartextPassword = 3,
    AuthenticationCryptPassword = 4,
    AuthenticationMD5Password = 5,
    AuthenticationSCMCredential = 6,
    AuthenticationGSS = 7,
    AuthenticationGSSContinue = 8,
    AuthenticationSSPI = 9,
    AuthenticationSASL = 10,
    AuthenticationSASLContinue = 11,
    AuthenticationSASLFinal = 12
}