#include "lip_opcua_server_cfg_defs.h"
#if LIP_OPCUA_SERVER_ENABLE
#include "lip_opcua_server_session.h"
#include "monitems/lip_opcua_server_monitem.h"
#include "lip_opcua_sc.h"
#include "lip/app/opcua/types/lip_opcua_nodeid.h"
#include "lip/app/opcua/lip_opcua_err_codes.h"
#include "lip/app/opcua/server/req/lip_opcua_server_req.h"
#include "lip/app/opcua/server/req/lip_opcua_server_reqproc.h"
#include "lip/app/opcua/server/req/svcsets/lip_opcua_server_svcset_subscription.h"
#include "lip/app/opcua/server/lip_opcua_server_con.h"
#include "ltimer.h"


typedef enum {
    LIP_OPCUA_SESSION_STATE_CLOSED,
    LIP_OPCUA_SESSION_STATE_OPENED,
    LIP_OPCUA_SESSION_STATE_ACTIVATED,
} t_lip_opcua_session_state;



typedef struct st_lip_opcua_session_ctx {
    struct st_lip_opcua_sc_ctx *cur_sc; /* SecureChannel, связанный с данной сессией */
    t_lip_opcua_nodeid nodeid; /* node-id для самой сессии */
    t_lip_opcua_nodeid token; /* node-id токена для проверки пользователя */
    t_ltimer tout_tmr; /* таймер на закрыте сессии по неактивности */

    t_lip_opcua_session_state state;
#if LIP_OPCUA_SERVER_SUBS_ENABLE
    uint8_t subs_cnt;
    uint16_t pubreq_cnt;
    uint16_t pubreq_getpos;
    t_lip_opcua_srvsub_ctx subs[LIP_OPCUA_SERVER_SESSION_SUBS_MAX_CNT];    
    t_lip_opcua_req_resp_info pubreqs[LIP_OPCUA_SERVER_SESSION_PUBREQ_MAX_CNT];    
#endif
} t_lip_opcua_session_ctx;

LIP_OPCUA_SERVER_SESSION_MEM(static t_lip_opcua_session_ctx f_sessions[LIP_OPCUA_SERVER_SESSION_CNT]);

static uint32_t f_last_sessin_id;

static inline void f_dequeu_pubreq(struct st_lip_opcua_session_ctx *session) {
    if (++session->pubreq_getpos == LIP_OPCUA_SERVER_SESSION_PUBREQ_MAX_CNT) {
        session->pubreq_getpos = 0;
    }
    --session->pubreq_cnt;
}

static void f_abort_pubreq(struct st_lip_opcua_session_ctx *session, t_lip_opcua_err status) {
    struct st_lip_opcua_con *con = lip_opcua_sc_get_con(session->cur_sc);
    if (con) {
        lip_opcua_server_resp_fault(con, &session->pubreqs[session->pubreq_getpos], status);
    }
    f_dequeu_pubreq(session);
}

static inline t_lip_opcua_err f_check_session_activated(struct st_lip_opcua_session_ctx *session) {
    t_lip_opcua_err err;
    if (!session || (session->state == LIP_OPCUA_SESSION_STATE_CLOSED)) {
        err = LIP_OPCUA_ERR_BAD_SESSION_CLOSED;
    } else if (session->state != LIP_OPCUA_SESSION_STATE_ACTIVATED) {
        err = LIP_OPCUA_ERR_BAD_SESSION_NOT_ACTIVATED;
    } else {
        err = LIP_OPCUA_ERR_GOOD;
    }
    return err;
}


void lip_opcua_sessions_init(void) {
    memset(f_sessions, 0, sizeof(f_sessions));
}
void lip_opcua_sessions_pull(void) {
    for (int session_idx = 0; (session_idx < LIP_OPCUA_SERVER_SESSION_CNT); ++session_idx) {
        t_lip_opcua_session_ctx *session = &f_sessions[session_idx];
        if ((session->state != LIP_OPCUA_SESSION_STATE_CLOSED) && ltimer_expired(&session->tout_tmr)) {
            lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_WARNING_MEDIUM, 0, "lip opcua: session %d closed by timeout!\n", session->nodeid.intval);
            lip_opcua_session_close(session, true);
        }

        if (session->state == LIP_OPCUA_SESSION_STATE_ACTIVATED) {
            if (session->subs_cnt > 0) {
                int proc_subs = 0;
                for (int sub_idx = 0; (sub_idx < LIP_OPCUA_SERVER_SESSION_SUBS_MAX_CNT) && (proc_subs < session->subs_cnt); ++sub_idx) {
                    t_lip_opcua_srvsub_ctx *sub = &session->subs[sub_idx];
                    if (sub->state != LIP_OPCUA_SRVSUB_STATE_CLOSED) {
                        lip_opcua_server_sub_pull(sub, session);
                        ++proc_subs;
                    }
                }
            }
        }
    }
}

void lip_opcua_sessions_close(void) {

}

void lip_opcua_session_clear_sc(struct st_lip_opcua_session_ctx *session) {
    if (session) {
        session->cur_sc = NULL;
        lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_DEBUG_MEDIUM, 0, "lip opcua: session %d close sc!\n", session->nodeid.intval);
    }
}

t_lip_opcua_err lip_opcua_check_user_auth_token(struct st_lip_opcua_session_ctx *session, const t_lip_opcua_nodeid *token_node_id) {
    t_lip_opcua_err err = LIP_OPCUA_ERR_GOOD;
    if (session && (session->state != LIP_OPCUA_SESSION_STATE_CLOSED)) {
        if (!lip_opcua_nodeid_is_eq(&session->token, token_node_id)) {
            err = LIP_OPCUA_ERR_BAD_IDENTITY_TOKEN_INVALID;
        } else {
            ltimer_restart(&session->tout_tmr);
        }
    }
    return err;
}

t_lip_opcua_err lip_opcua_session_create(const t_lip_opcua_session_create_params *params, t_lip_opcua_session_create_result *result) {
    t_lip_opcua_err err =  LIP_OPCUA_ERR_GOOD;
    t_lip_opcua_session_ctx *fnd_session = NULL;
    /* В первую очередь ищем закрытую сессию */
    for (int session_idx = 0; (session_idx < LIP_OPCUA_SERVER_SESSION_CNT) && (fnd_session == NULL); ++session_idx) {
        t_lip_opcua_session_ctx *ctx = &f_sessions[session_idx];
        if (ctx->state == LIP_OPCUA_SESSION_STATE_CLOSED) {
            fnd_session = ctx;
        }
    }

    /* если не нашли, то пробуем найти сессию без SecureChannel */
    if (!fnd_session) {
        uint32_t cur_delta = 0;
        for (int session_idx = 0; (session_idx < LIP_OPCUA_SERVER_SESSION_CNT); ++session_idx) {
            t_lip_opcua_session_ctx *session = &f_sessions[session_idx];
            if (!session->cur_sc) {
                /* закрываем саму старую сессию без sc. в качестве признака старости смотрим
                 * насколько ее id меньше текущего, с самой большой разницей - самя старая */
                uint32_t new_delta = f_last_sessin_id - session->nodeid.intval;
                if (!fnd_session || (new_delta > cur_delta)) {
                    fnd_session = session;
                }
            }
        }

        if (fnd_session) {
            lip_opcua_session_close(fnd_session, true);
        }
    }

    if (!fnd_session) {
        err = LIP_OPCUA_ERR_BAD_TOO_MANY_SESSIONS;
    } else {
        fnd_session->state = LIP_OPCUA_SESSION_STATE_OPENED;
        fnd_session->nodeid.type = LIP_OPCUA_NODEID_TYPE_INT;
        fnd_session->nodeid.ns_idx = 2;
        fnd_session->nodeid.intval = ++f_last_sessin_id;

        fnd_session->token.type = LIP_OPCUA_NODEID_TYPE_INT;
        fnd_session->token.ns_idx = 2;
        fnd_session->token.intval = ++f_last_sessin_id;

#if LIP_OPCUA_SERVER_SUBS_ENABLE
        fnd_session->subs_cnt = 0;
        fnd_session->pubreq_getpos = 0;
        fnd_session->pubreq_cnt = 0;
#endif

        fnd_session->cur_sc = params->sc;


        result->session_id = fnd_session->nodeid;
        result->token_id = fnd_session->token;
        result->sessionTout = params->sessionTout;
#ifdef LIP_OPCUA_SERVER_SESSION_MAX_TOUT
        if (result->sessionTout > LIP_OPCUA_SERVER_SESSION_MAX_TOUT) {
            result->sessionTout = LIP_OPCUA_SERVER_SESSION_MAX_TOUT;
        }
#endif
        result->maxReqSize = 0;
        ltimer_set_ms(&fnd_session->tout_tmr, result->sessionTout);

        lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_DEBUG_MEDIUM, 0, "lip opcua: session %d opened, tout %d!\n", fnd_session->nodeid.intval, result->sessionTout);

        lip_opcua_sc_set_session(params->sc, fnd_session);
    }

    return err;
}

t_lip_opcua_err lip_opcua_session_activate(t_lip_opcua_session_ctx *session, const t_lip_opcua_session_activate_params *params, t_lip_opcua_string_ptr *nonce) {
    t_lip_opcua_err err =  LIP_OPCUA_ERR_GOOD;
    if (!session || (session->state == LIP_OPCUA_SESSION_STATE_CLOSED)) {
        err = LIP_OPCUA_ERR_BAD_SESSION_CLOSED;
    } else {
        session->state = LIP_OPCUA_SESSION_STATE_ACTIVATED;
        nonce->data = NULL;
        nonce->size = -1;
    }


    return err;
}

t_lip_opcua_err lip_opcua_session_close(struct st_lip_opcua_session_ctx *session, bool deleteSubs) {
    t_lip_opcua_err err =  LIP_OPCUA_ERR_GOOD;
    if (!session || (session->state == LIP_OPCUA_SESSION_STATE_CLOSED)) {
        err = LIP_OPCUA_ERR_BAD_SESSION_CLOSED;
    } else {
        /** @todo по идее удаление subcription нужно только по deleteSubs, но не совсем пока
         *  понятно, что тогда делать с незакрытыми подписями */
        for (int sub_idx = 0; sub_idx < LIP_OPCUA_SERVER_SESSION_SUBS_MAX_CNT; ++sub_idx) {
            t_lip_opcua_srvsub_ctx *check_sub = &session->subs[sub_idx];
            if (check_sub->state != LIP_OPCUA_SRVSUB_STATE_CLOSED) {
                lip_opcua_server_sub_delete(check_sub);
            }
        }
        session->subs_cnt = 0;

        lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_DEBUG_MEDIUM, 0, "lip opcua: session %d closed!\n", session->nodeid.intval);
        lip_opcua_sc_set_session(session->cur_sc, NULL);
        session->state = LIP_OPCUA_SESSION_STATE_CLOSED;
    }
    return err;
}

#if LIP_OPCUA_SERVER_SUBS_ENABLE
t_lip_opcua_err lip_opcua_session_sub_create(struct st_lip_opcua_session_ctx *session, t_lip_opcua_subscription_params *params, bool publish_en, t_lip_opcua_intid *subid) {
    t_lip_opcua_err err =  f_check_session_activated(session);
    if (err == LIP_OPCUA_ERR_GOOD) {
        t_lip_opcua_srvsub_ctx *sub = NULL;
        for (int sub_idx = 0; (sub_idx < LIP_OPCUA_SERVER_SESSION_SUBS_MAX_CNT) && (sub == NULL); ++sub_idx) {
            t_lip_opcua_srvsub_ctx *check_sub = &session->subs[sub_idx];
            if (check_sub->state == LIP_OPCUA_SRVSUB_STATE_CLOSED) {
                sub = check_sub;
            }
        }

        if (!sub) {
            err = LIP_OPCUA_ERR_BAD_TOO_MANY_SUBSCRIPTIONS;
        } else {
            err = lip_opcua_server_sub_create(sub, params, publish_en);
            if (err == LIP_OPCUA_ERR_GOOD) {
                ++session->subs_cnt;
                if (subid) {
                    *subid = sub->id;
                }
            }
        }
    }
    return err;
}

t_lip_opcua_err lip_opcua_session_sub_delete(struct st_lip_opcua_session_ctx *session, t_lip_opcua_intid subid) {
    t_lip_opcua_srvsub_ctx *sub = NULL;
    t_lip_opcua_err err = lip_opcua_session_sub_get(session, subid, &sub);
    if (err == LIP_OPCUA_ERR_GOOD) {
        lip_opcua_server_sub_delete(sub);
        --session->subs_cnt;
    }
    return err;
}

t_lip_opcua_err lip_opcua_session_sub_get(struct st_lip_opcua_session_ctx *session, t_lip_opcua_intid subid, struct st_lip_opcua_srvsub_ctx **sub) {
    t_lip_opcua_err err =  f_check_session_activated(session);
    if (err == LIP_OPCUA_ERR_GOOD) {
        t_lip_opcua_srvsub_ctx *fnd_sub = NULL;
        for (int sub_idx = 0; (sub_idx < LIP_OPCUA_SERVER_SESSION_SUBS_MAX_CNT) && (fnd_sub == NULL); ++sub_idx) {
            t_lip_opcua_srvsub_ctx *check_sub = &session->subs[sub_idx];
            if ((check_sub->state != LIP_OPCUA_SRVSUB_STATE_CLOSED) && (check_sub->id == subid)) {
                fnd_sub = check_sub;
            }
        }

        if (!fnd_sub) {
            err = LIP_OPCUA_ERR_BAD_SUBSCRIPTION_ID_INVALID;
        }

        if (sub) {
            *sub = fnd_sub;
        }
    }


    return err;
}

t_lip_opcua_err lip_opcua_session_publish(struct st_lip_opcua_session_ctx *session, const t_lip_opcua_req_resp_info *req_info) {
    t_lip_opcua_err err =  f_check_session_activated(session);
    if (err == LIP_OPCUA_ERR_GOOD) {
        if (session->subs_cnt == 0) {
            err = LIP_OPCUA_ERR_BAD_NO_SUBSCRIPTION;
        } else {
            if (session->pubreq_cnt == 0) {
                /* сохраняем в общую очередь */
                session->pubreqs[session->pubreq_getpos] = *req_info;
                ++session->pubreq_cnt;
                lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_DEBUG_MEDIUM, 0, "lip opcua: session %d: queue first pubreq!\n", session->nodeid.intval);

                /* ищем среди подписок, нет ли тех, которые уже готовы обработать pubrequest */
                for (int sub_idx = 0; (sub_idx < session->subs_cnt) && (session->pubreq_cnt > 0); ++sub_idx) {
                    t_lip_opcua_srvsub_ctx *sub = &session->subs[sub_idx];
                    if (sub->state != LIP_OPCUA_SRVSUB_STATE_CLOSED) {
                        lip_opcua_server_sub_proc_publish(sub, session);
                    }
                }
            } else {
                if (session->pubreq_cnt == LIP_OPCUA_SERVER_SESSION_PUBREQ_MAX_CNT) {
                    lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_WARNING_LOW, 0, "lip opcua: session %d: pubreq overflow!\n", session->nodeid.intval);
                    f_abort_pubreq(session, LIP_OPCUA_ERR_BAD_TOO_MANY_PUBLISH_REQUESTS);
                }
                int put_pos = session->pubreq_getpos + session->pubreq_cnt;
                if (put_pos >= LIP_OPCUA_SERVER_SESSION_PUBREQ_MAX_CNT)
                    put_pos -= LIP_OPCUA_SERVER_SESSION_PUBREQ_MAX_CNT;

                session->pubreqs[put_pos] = *req_info;
                ++session->pubreq_cnt;

                lip_printf(LIP_LOG_MODULE_OPCUA, LIP_LOG_LVL_DEBUG_MEDIUM, 0, "lip opcua: session %d: queue pubreq %d (total %d)!\n",
                           session->nodeid.intval,  req_info->req_id, session->pubreq_cnt);
            }
        }
    }
    return err;
}

bool lip_opcua_session_has_publish_req(struct st_lip_opcua_session_ctx *session) {
    return session->pubreq_cnt > 0;
}

t_lip_proc_buf *lip_opcua_server_session_publish_resp_prepare(struct st_lip_opcua_session_ctx *session, t_lip_opcua_intid subid, t_lip_proc_buf *more_notify_buf) {
    t_lip_proc_buf *resp_buf = NULL;
    if (session->pubreq_cnt > 0) {
        struct st_lip_opcua_con *con = lip_opcua_sc_get_con(session->cur_sc);
        if (con) {           
            resp_buf = lip_opcua_server_subscription_pub_resp_prepare(con, &session->pubreqs[session->pubreq_getpos], subid, more_notify_buf);
        }
    }
    return resp_buf;
}

t_lip_opcua_err lip_opcua_server_session_publish_resp_finish(struct st_lip_opcua_session_ctx *session, t_lip_proc_buf *buf) {
    t_lip_opcua_err err = LIP_OPCUA_ERR_GOOD;
    if (session->pubreq_cnt > 0) {
        struct st_lip_opcua_con *con = lip_opcua_sc_get_con(session->cur_sc);
        if (con) {
            err = lip_opcua_server_subscription_pub_resp_finish(con, buf);
            if (err == LIP_OPCUA_ERR_GOOD) {
                f_dequeu_pubreq(session);
            }
        } else {
            err = LIP_OPCUA_ERR_BAD_SERVER_NOT_CONNECTED;
        }
    } else {
        err = LIP_OPCUA_ERR_BAD_INTERNAL_ERROR;
    }
    return err;
}

#endif

#endif
