/*
 * Created by jiaqiang.ye@lnxall.com
 *
 * Simple OPC-UA remote TAG implementation, for LC
 *
 * 2024/05/21
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "open62541.h"
#include "remote_ua_client.h"

void lc_opcua_option_default(struct cli_opcua_option * loo)
{
    if (loo != NULL) {
		loo->uaurl = "opc.tcp://127.0.0.1:4841";
		loo->username = "admin";
		loo->passwd = "lnxall123";
	}
}

static UA_ByteString loadFile(const char *const path) {
    UA_ByteString fileContents = UA_BYTESTRING_NULL;

    /* Open the file */
    FILE *fp = fopen(path, "rb");
    if(!fp) {
        errno = 0; /* We read errno also from the tcp layer... */
        return fileContents;
    }

    /* Get the file length, allocate the data and read */
    fseek(fp, 0, SEEK_END);
    fileContents.length = (size_t)ftell(fp);
    fileContents.data = (UA_Byte *)UA_malloc(fileContents.length * sizeof(UA_Byte));
    if(fileContents.data) {
        fseek(fp, 0, SEEK_SET);
        size_t read = fread(fileContents.data, sizeof(UA_Byte), fileContents.length, fp);
        if(read != fileContents.length)
            UA_ByteString_clear(&fileContents);
    } else {
        fileContents.length = 0;
    }
    fclose(fp);

    return fileContents;
}

/* SAN URI is ASN.1 context-specific tag 6 (0x86) followed by an IA5String. */
static int application_uri_from_cert(UA_String *dst, const UA_ByteString *der)
{
    size_t i;

    if (dst == NULL || der == NULL || der->data == NULL)
        return -1;

    for (i = 0; i + 6 < der->length; i++) {
        unsigned int len;

        if (der->data[i] != 0x86)
            continue;
        len = der->data[i + 1];
        if (len < 5 || len > 200 || i + 2 + len > der->length)
            continue;
        if (memcmp(der->data + i + 2, "urn:", 4) != 0)
            continue;
        {
            char tmp[256];
            memcpy(tmp, der->data + i + 2, len);
            tmp[len] = '\0';
            UA_String_clear(dst);
            *dst = UA_STRING_ALLOC(tmp);
            return 0;
        }
    }
    return -1;
}

static UA_StatusCode
lc_skip_verify_application_uri(const UA_CertificateVerification *cv,
                               const UA_ByteString *certificate,
                               const UA_String *applicationURI)
{
    (void)cv;
    (void)certificate;
    (void)applicationURI;
    return UA_STATUSCODE_GOOD;
}

static UA_StatusCode
lc_skip_verify_certificate(const UA_CertificateVerification *cv,
                           const UA_ByteString *certificate)
{
    (void)cv;
    (void)certificate;
    return UA_STATUSCODE_GOOD;
}

static int lc_wait_session_active(UA_Client *uacli, int timeout_ms)
{
    int waited = 0;

    while (waited < timeout_ms) {
        UA_SecureChannelState secst = UA_SECURECHANNELSTATE_CLOSED;
        UA_SessionState sesst = UA_SESSIONSTATE_CLOSED;
        UA_StatusCode status = UA_STATUSCODE_GOOD;

        UA_Client_run_iterate(uacli, 100);
        UA_Client_getState(uacli, &secst, &sesst, &status);
        if (status == UA_STATUSCODE_GOOD &&
            secst == UA_SECURECHANNELSTATE_OPEN &&
            sesst == UA_SESSIONSTATE_ACTIVATED)
            return 0;
        waited += 100;
    }
    return -1;
}

static void lc_configure_client(UA_ClientConfig *ccfg,
                                const UA_ByteString *certificate,
                                const UA_ByteString *privateKey,
                                const UA_ByteString *trustList,
                                size_t trustListSize,
                                UA_MessageSecurityMode mode)
{
    UA_StatusCode retval;

    ccfg->timeout = 30000;
    ccfg->connectivityCheckInterval = 0;

    retval = UA_ClientConfig_setDefaultEncryption(ccfg, *certificate, *privateKey,
        trustList, trustListSize, NULL, 0);
    if (retval != UA_STATUSCODE_GOOD) {
        fprintf(stderr, "setDefaultEncryption failed: %s\n", UA_StatusCode_name(retval));
        ccfg->securityMode = UA_MESSAGESECURITYMODE_NONE;
        return;
    }

    application_uri_from_cert(&ccfg->clientDescription.applicationUri, certificate);
    /* Local CLI: skip PKI hostname/URI checks that 1.4 enforces. */
    if (ccfg->certificateVerification.verifyCertificate != NULL)
        ccfg->certificateVerification.verifyCertificate = lc_skip_verify_certificate;
    if (ccfg->certificateVerification.verifyApplicationURI != NULL)
        ccfg->certificateVerification.verifyApplicationURI = lc_skip_verify_application_uri;
    ccfg->securityMode = mode;
}

static UA_Client *lc_try_connect(const struct cli_opcua_option *loo,
                                 UA_MessageSecurityMode mode,
                                 int use_auth,
                                 const UA_ByteString *certificate,
                                 const UA_ByteString *privateKey,
                                 const UA_ByteString *trustList)
{
    UA_Client *uacli;
    UA_ClientConfig *ccfg;
    UA_StatusCode retval;

    uacli = UA_Client_new();
    if (uacli == NULL)
        return NULL;

    ccfg = UA_Client_getConfig(uacli);
    lc_configure_client(ccfg, certificate, privateKey, trustList, 1, mode);

    if (use_auth && loo->username && loo->passwd) {
        retval = UA_ClientConfig_setAuthenticationUsername(ccfg, loo->username, loo->passwd);
        if (retval != UA_STATUSCODE_GOOD) {
            fprintf(stderr, "setAuthenticationUsername failed: %s\n", UA_StatusCode_name(retval));
            UA_Client_delete(uacli);
            return NULL;
        }
    }

    retval = UA_Client_connect(uacli, loo->uaurl);
    if (retval == UA_STATUSCODE_GOOD && lc_wait_session_active(uacli, 10000) != 0)
        retval = UA_STATUSCODE_BADCONNECTIONCLOSED;
    if (retval != UA_STATUSCODE_GOOD) {
        fprintf(stderr, "Connect %s with securityMode=%d failed: %s\n",
            loo->uaurl, (int)mode, UA_StatusCode_name(retval));
        UA_Client_disconnect(uacli);
        UA_Client_delete(uacli);
        return NULL;
    }
    return uacli;
}

int lc_client_closed(UA_Client * uacli, int * errorp)
{
    UA_StatusCode status = UA_STATUSCODE_GOOD;
    UA_SessionState sesst = UA_SESSIONSTATE_CLOSED;
    UA_SecureChannelState secst = UA_SECURECHANNELSTATE_CLOSED;

    if (errorp != NULL)
        *errorp = 0;
    if (uacli == NULL)
        return 1;

    /* New open62541 no longer exposes the raw TCP fd (UA_Client_Rawsock). */
    UA_Client_getState(uacli, &secst, &sesst, &status);
    if (status != UA_STATUSCODE_GOOD ||
        secst == UA_SECURECHANNELSTATE_CLOSED ||
        secst == UA_SECURECHANNELSTATE_CLOSING ||
        sesst == UA_SESSIONSTATE_CLOSED ||
        sesst == UA_SESSIONSTATE_CLOSING) {
        if (errorp != NULL)
            *errorp = EPIPE;
        return 1;
    }
    return 0;
}

void lc_client_free(UA_Client * uacli)
{
    if (uacli != NULL) {
        UA_Client_disconnect(uacli);
        UA_Client_delete(uacli);
    }
}

UA_Client * lc_connect_opcua(const struct cli_opcua_option * loo)
{
    UA_Client *uacli;
    UA_ByteString certificate;
    UA_ByteString privateKey;
    UA_ByteString trustList;

    certificate = loadFile(CLIENT_CERT_CERT);
    privateKey = loadFile(CLIENT_CERT_KEY);
    trustList = loadFile(SERVER_CERT_CERT);
    if (certificate.length == 0 || privateKey.length == 0)
        fprintf(stderr, "Warning, client cert/key missing: %s %s\n",
            CLIENT_CERT_CERT, CLIENT_CERT_KEY);

    /* 1.4: INVALID would GetEndpoints before the channel is OPEN and time out. */
    uacli = lc_try_connect(loo, UA_MESSAGESECURITYMODE_SIGNANDENCRYPT, 1,
        &certificate, &privateKey, &trustList);
    if (uacli == NULL) {
        fprintf(stderr, "Retry connect with SecurityPolicy None (anonymous)\n");
        uacli = lc_try_connect(loo, UA_MESSAGESECURITYMODE_NONE, 0,
            &certificate, &privateKey, &trustList);
    }

    UA_ByteString_clear(&certificate);
    UA_ByteString_clear(&privateKey);
    UA_ByteString_clear(&trustList);

    if (uacli == NULL)
        fprintf(stderr, "Failed to connect to %s\n", loo->uaurl);
    return uacli;
}
