using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Flurl;
using ICSharpCode.SharpZipLib.Tar;
using InnovEnergy.App.OpenVpnCertificatesServer.PKI;
using InnovEnergy.Lib.Utils;
using InnovEnergy.Lib.Victron.VictronVRM;
using Org.BouncyCastle.Crypto;
using static InnovEnergy.App.OpenVpnCertificatesServer.PKI.CertificateAuthority;

namespace InnovEnergy.App.OpenVpnCertificatesServer;

// export SolutionDir=$(pwd)
// dotnet publish OpenVpnCertificatesServer.csproj -c Release -r linux-x64 -p:PublishSingleFile=true --self-contained true  && scp bin/Release/net6.0/linux-x64/publish/OpenVpnCertificatesServer ig@salidomo.innovenergy.ch:~/get_cert/get_cert
// scp bin/Release/net6.0/linux-x64/publish/token.json ig@salidomo.innovenergy.ch:~/get_cert/token.json


// http://localhost:4000/get_cert?machine_serial=HQ2032UAWYM
// http://localhost:4000/get_cert?unique_id=985dadd0cf29

// quotes!!
// wget 'http://localhost:4000/get_cert?name=MYNAME&pw=MwBRbQb3QaX7l9XIaakq'  

[SuppressMessage("Trimming", "IL2026:Members annotated with \'RequiresUnreferencedCodeAttribute\' require dynamic access otherwise can break functionality when trimming application code")]
public static class Program
{
    // TODO: use fody weaver to obfuscate strings?

    private const String VpnSubnet  = "10.2.";
    private const String VpnGateway = "10.2.0.1";

    private const String VpnDir     = "/etc/openvpn/server/Salino";
    private const String CcdDir     = VpnDir + "/ccd";
    private const String CertsDir   = VpnDir + "/certs";
    private const String Endpoint   = "http://localhost:4000/";

    private static AccToken? ReadAccessToken()
    {
        var content = File.ReadAllText("./token.json");
        
        return JsonSerializer.Deserialize<AccToken>(content);
    }


    private const String ManualPw   = "MwBRbQb3QaX7l9XIaakq";
    
    private const String QueryPath               = "/get_cert";
    private const String NameQueryParam          = "name";
    private const String UniqueIdQueryParam      = "unique_id";
    private const String MachineSerialQueryParam = "machine_serial";
    private const String PwdQueryParam           = "pw";
    

    private const Int64 MachineSerialAttribute = 556; // TODO: enum
    
    private static readonly (String, String)? InvalidRequest = null;
    
    private static IEnumerable<String> CcdFiles => Directory.GetFiles(CcdDir);

    public static void Main(String[] args) => Endpoint.ServeGet(GetCert);

    private static Byte[]? GetCert(Url url)
    {
        var request = ParseRequest(url).Result;

        if (!request.HasValue)
            return null;

        var (ccdName, humanReadableName) = request.Value;

        var ip      = GetNextFreeIp();
        var keyPair = CreateKeyPair();
        var cert    = CreateCertificate(ccdName, keyPair).Apply(Pem.Encode);
        var tar     = PackTar(cert, humanReadableName, keyPair, ip);

        WriteServerCertificateFiles(ccdName, ip, tar);

        Console.WriteLine($"returning certificate for {humanReadableName}\n");
        return tar;
    }

    private static Byte[] PackTar(String certificate, String certificateName, AsymmetricCipherKeyPair keys, String ip)
    {
        Console.WriteLine("packing tar"); 

        var publicKey  = keys.Public .Apply(Pem.Encode);
        var privateKey = keys.Private.Apply(Pem.Encode);

        using var ms = new MemoryStream();

        using (var ts = new TarOutputStream(ms, Encoding.UTF8) {IsStreamOwner = false})
        {
            ts.WriteTextFile("installation-ip"   , ip);
            ts.WriteTextFile("installation-name" , certificateName);
            ts.WriteTextFile("ca-certificate"    , CaCertificatePem.Replace("  ", "")); // remove leading spaces
            ts.WriteTextFile("client-certificate", certificate);
            ts.WriteTextFile("client-key.pub"    , publicKey);
            ts.WriteTextFile("client-key"        , privateKey);
            ts.WriteTextFile("innovenergy.conf"  , Files.OvpnConf);
            ts.WriteTextFile("README.txt"        , Files.ReadMeTxt);
        }
        // MUST Dispose to commit data to underlying MemoryStream !!!!

        return ms.ToArray();
    }


    private static async Task<(String CcdName, String HumanReadableName)?> ParseRequest(Url? url = null)
    {
        Console.WriteLine($"got request {url}");

        if (!url.Path.EndsWith(QueryPath))
        {
            Console.WriteLine("Malformed request\n");
            return InvalidRequest;
        }
        
        var ps = url.QueryParams;

        var ccdName = ps.FirstOrDefault(UniqueIdQueryParam)     ?.ToString() ?? 
                      ps.FirstOrDefault(MachineSerialQueryParam)?.ToString() ??
                      ps.FirstOrDefault(NameQueryParam)         ?.ToString();

        if (!IsCcdNameValid(ccdName))
        {
            Console.WriteLine($"Invalid ccd name {ccdName}\n");
            return InvalidRequest;
        }

        if (!IsCcdNameAvailable(ccdName))
        {
            Console.WriteLine($"ccd name {ccdName} is already in use\n");
            return InvalidRequest;
        }

        return ps.Contains(NameQueryParam)          ? ParseManualRequest(ps, ccdName!)
             : ps.Contains(UniqueIdQueryParam)      ? await LookupInstallationNameByUniqueId(ccdName!) 
             : ps.Contains(MachineSerialQueryParam) ? await LookupInstallationNameByMachineSerial(ccdName!)
             : InvalidRequest;
    }

    private static async Task<(String ccdName, String humanReadableName)?> LookupInstallationNameByUniqueId(String uniqueId)
    {
        Console.WriteLine($"looking up unique id {uniqueId} on VRM");

        //var installationName = await LookupInstallationNameByUniqueId(ccdName);
        var token = ReadAccessToken();

        var vrm = VrmAccount.Token(token.idUser, token.token);  

        
        var installations = await vrm.GetInstallations();
        
        var installationName = installations
                              .Where(i => i.UniqueId == uniqueId)
                              .Select(i => i.Name)
                              .FirstOrDefault();

        return !String.IsNullOrWhiteSpace(installationName) 
             ? (uniqueId, installationName) 
             : null;
    }
    
    private static async Task<(String ccdName, String humanReadableName)?> LookupInstallationNameByMachineSerial(String ccdName)
    {
        Console.WriteLine($"looking up {ccdName} on VRM");
        var token = ReadAccessToken();

        using var vrm = VrmAccount.Token(token.idUser, token.token);  

        var installations = await vrm.GetInstallations();

        foreach (var installation in installations)
        {
            var details = await installation.GetDetails();

            var id = details.MachineSerial();

            if (id == ccdName)
                return (ccdName, installation.Name);
        }

        Console.WriteLine($"not found");
        return null;
    }


    private static (String ccdName, String humanReadableName)? ParseManualRequest(QueryParamCollection ps, String ccdName)
    {
        Console.WriteLine("parsing manual request");

        var pw = ps.FirstOrDefault(PwdQueryParam)?.ToString();

        if (pw == ManualPw)
            return (ccdName, ccdName); // ccd and hr name are the same in this case

        Console.WriteLine("Malformed request\n");

        return null;
    }


    private static Boolean IsCcdNameValid(String? ccdName)
    {
        return ccdName != null && ccdName.All(IsValidChar);

        Boolean IsValidChar(Char c) => Char.IsLetterOrDigit(c) || c is '_' or '-';
    }
    
    private static Boolean IsCcdNameAvailable(String? ccdName)
    {
        Console.WriteLine($"checking ccdName {ccdName ?? ""}");

        return ccdName != null && !CcdFiles
                                  .Select(Path.GetFileName)
                                  .Contains(ccdName);
    }

    private static void WriteServerCertificateFiles(String ccdName, String ip, Byte[] certsTar)
    {
        Console.WriteLine($"storing certificate {ccdName} in {CertsDir}");

        var ccdContent = $"ifconfig-push {ip} {VpnGateway}";
        var ccdFile    = $"{CcdDir}/{ccdName}";
        var tarFile    = $"{CertsDir}/{ccdName}.tar";

        File.WriteAllText (ccdFile, ccdContent);
        File.WriteAllBytes(tarFile, certsTar);
    }


    // TODO: this could be made simpler using OrderBy/ThenBy
    private static Int32 GetId(String ccdFile)
    {
        var ip = File
                .ReadAllLines(ccdFile)
                .Select(l => l.Trim())
                .SingleOrDefault(l => l.StartsWith("ifconfig-push"))?
                .Split(" ", StringSplitOptions.RemoveEmptyEntries)
                .ElementAtOrDefault(1);

        if (ip == null)
            return 0;

        var bytes = ip.Split(".");

        var hi = bytes.ElementAtOrDefault(2);
        var lo = bytes.ElementAtOrDefault(3);

        // TODO: GetIp => tuple (a,b,hi,lo) ?

        if (hi == null || lo == null)
            return 0;

        if (!Byte.TryParse(hi, out var hiByte) || !Byte.TryParse(lo, out var loByte))
            return 0;

        return hiByte * 256 + loByte;
    }


    // TODO: this could be made simpler using OrderBy/ThenBy
    private static String GetNextFreeIp()
    {
        // 10.2.hi.lo
        // id := 256 * hi + lo

        var id = CcdFiles.Max(GetId) + 1;

        var lo = id % 256;

        // x.y.z.0 and x.y.z.255 are reserved
        if (lo == 0)   id += 1;
        if (lo == 255) id += 2;

        var hi = id / 256;
        lo = id % 256;

        Debug.Assert(lo != 0);
        Debug.Assert(lo != 255);
        Debug.Assert(hi != 255);

        return $"{VpnSubnet}{hi}.{lo}";
    }
}