#include <expat.h>

#include <cstring>
#include <cerrno>
#include <cassert>
#include <iostream>

#include <boost/thread/mutex.hpp>

// TODO: Fix this shit!
#include "../../../admins.h"

#include "common.h"
#include "proto.h"
#include "config_thread.h"
#include "root_parser.h"

void DumpCrypto(const char * data, size_t size)
{
    std::string dumpstr = "";
    for (unsigned i = 0; i < size; ++i) {
        std::string ch;
        strprintf(&ch, "%x", *(data + i));
        dumpstr += ch;
    }
    printfd(__FILE__, "Crypto dump: '%s'\n", dumpstr.c_str());
}

CONFIG_THREAD::CONFIG_THREAD(ADMINS * a, TARIFFS * t, USERS * u, const SETTINGS * s)
    : sd(-1),
      done(false),
      state(ST_NOOP),
      respCode(RESP::OK),
      admins(a),
      tariffs(t),
      users(u),
      settings(s)
{
    /*printfd(__FILE__, "sizeof(REQ::HEADER) = %d\n", sizeof(REQ::HEADER));
    printfd(__FILE__, "sizeof(REQ::CRYPTO_HEADER) = %d\n", sizeof(REQ::CRYPTO_HEADER));
    printfd(__FILE__, "sizeof(RESP::HEADER) = %d\n", sizeof(RESP::HEADER));
    printfd(__FILE__, "sizeof(RESP::CRYPTO_HEADER) = %d\n", sizeof(RESP::CRYPTO_HEADER));*/
    assert(sizeof(REQ::HEADER) % 8 == 0);
    assert(sizeof(REQ::CRYPTO_HEADER) % 8 == 0);
    assert(sizeof(RESP::HEADER) % 8 == 0);
    assert(sizeof(RESP::CRYPTO_HEADER) % 8 == 0);

    iv = new unsigned char[8];
    memset(iv, 0, 8);
}

CONFIG_THREAD::CONFIG_THREAD(const CONFIG_THREAD & rvalue)
    : sd(rvalue.sd),
      remoteAddr(rvalue.remoteAddr),
      done(false),
      state(ST_NOOP),
      respCode(rvalue.respCode),
      admins(rvalue.admins),
      tariffs(rvalue.tariffs),
      users(rvalue.users),
      settings(rvalue.settings)
{
    assert(!rvalue.done);
    iv = new unsigned char[8];
    memcpy(iv, rvalue.iv, 8);
}

CONFIG_THREAD & CONFIG_THREAD::operator=(const CONFIG_THREAD & rvalue)
{
    assert(0 && "Never be here");
    return *this;
}

CONFIG_THREAD::~CONFIG_THREAD()
{
    //assert(done);
    delete[] iv;
}

void CONFIG_THREAD::operator() ()
{
    if (sd < 0) {
        printfd(__FILE__, "CONFIG_THREAD::operator()() Invalid socket descriptor\n");
        return;
    }

    if (ReadReq()) {
        Process();
    }

    WriteResp();

    close(sd);

    {
        boost::mutex::scoped_lock lock(mutex);
        done = true;
    }
}

bool CONFIG_THREAD::IsDone() const
{
    boost::mutex::scoped_lock lock(mutex);
    return done;
}

void CONFIG_THREAD::SetConnection(int sock, struct sockaddr_in sin)
{
    sd = sock;
    remoteAddr = sin;
}

bool CONFIG_THREAD::ReadBlock(void * dest, size_t & size, int timeout) const
{
    unsigned readSize = 0;
    char * ptr = static_cast<char *>(dest);
    while (readSize < size) {
        struct timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = timeout * 1000;

        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(sd, &rfds);

        int res = select(sd + 1, &rfds, NULL, NULL, &tv);
        /* Don't rely on the value of tv now! */

        if (res < 0) {
            printfd(__FILE__, "CONFIG_THREAD::ReadBlock() Select error: '%s'\n", strerror(errno));
            return false;
        }

        if (res == 0) {
            // Timeout
            size = readSize;
            return false;
        }

        res = read(sd, ptr + readSize, size - readSize);

        if (res == 0) { // EOF
            printfd(__FILE__, "CONFIG_THREAD::ReadBlock() EOF\n");
            return false;
        }

        // Ignore 'Interrupted system call' errors
        if (res < 0) {
            if (errno != EINTR) {
                printfd(__FILE__, "CONFIG_THREAD::ReadBlock() Read error: '%s'\n", strerror(errno));
                return false;
            } else {
                continue;
            }
        }

        readSize += res;
    }

    return true;
}

bool CONFIG_THREAD::WriteBlock(const void * source, size_t & size, int timeout) const
{
    const char * ptr = static_cast<const char *>(source);
    unsigned writeSize = 0;
    while (writeSize < size) {
        struct timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = timeout * 1000;

        fd_set wfds;
        FD_ZERO(&wfds);
        FD_SET(sd, &wfds);

        int res = select(sd + 1, NULL, &wfds, NULL, &tv);
        /* Don't rely on the value of tv now! */

        if (res < 0) {
            printfd(__FILE__, "CONFIG_THREAD::WriteBlock() Select error: '%s'\n", strerror(errno));
            return false;
        }

        if (res == 0) {
            // Timeout
            size = writeSize;
            return false;
        }

        res = write(sd, ptr + writeSize, size - writeSize);

        // Ignore 'Interrupted system call' errors
        if (res < 0 && errno != EINTR) {
            printfd(__FILE__, "CONFIG_THREAD::WriteBlock() Write error: '%s'\n", strerror(errno));
            return false;
        }

        writeSize += res;
    }

    return true;
}

bool CONFIG_THREAD::ReadReq()
{
    struct REQ::HEADER reqHeader;

    size_t size = sizeof(reqHeader);
    if (!ReadBlock(&reqHeader, size, 5000)) {
        state = ST_ERROR;
        message = "No request header within 5 sec";
        printfd(__FILE__, "CONFIG_THREAD::ReadReq() %s\n", message.c_str());
        return false;
    }

    if (strncmp(reqHeader.magic, PROTO_MAGIC, sizeof(reqHeader.magic))) {
        state = ST_ERROR;
        respCode = RESP::INVALID_MAGIC;
        message = "Invalid magic code in header";
        printfd(__FILE__, "CONFIG_THREAD::ReadReq() %s\n", message.c_str());
        return false;
    }

    uint32_t version = ntohl(reqHeader.version);
    if (version > (2 << 8 | 0)) {
        state = ST_ERROR;
        respCode = RESP::UNSUPPORTED_VERSION;
        message = "Unsupported version";
        printfd(__FILE__, "CONFIG_THREAD::ReadReq() %s (wanted: %d, actual: %d)\n", message.c_str(), (2 << 8 | 0), version);
        return false;
    }

    versionMinor = version & 0x0000FFFF;
    versionMajor = (version >> 8) & 0x0000FFFF;

    reqHeader.login[sizeof(reqHeader.login) - 1] = 0;

    login = reqHeader.login;

    if (!CheckLogin(login, password)) {
        state = ST_ERROR;
        respCode = RESP::INVALID_CREDENTIALS;
        message = "Unknown login";
        printfd(__FILE__, "CONFIG_THREAD::ReadReq() %s\n", message.c_str());
        return false;
    }

    return ReceiveData();
}

bool CONFIG_THREAD::ReceiveData()
{
    unsigned char buffer[sizeof(struct REQ::CRYPTO_HEADER)];
    //unsigned char iv[] = "00000000";
    size_t size = sizeof(struct REQ::CRYPTO_HEADER);

    if (!ReadBlock(buffer, size, 5000)) {
        state = ST_ERROR;
        message = "No crypto header within 5 secs";
        printfd(__FILE__, "CONFIG_THREAD::ReceiveData() %s\n", message.c_str());
        return false;
    }

    BF_set_key(&key, password.length(), reinterpret_cast<const unsigned char *>(password.c_str()));

    struct REQ::CRYPTO_HEADER reqCryptoHeader;

    BF_cbc_encrypt(buffer, reinterpret_cast<unsigned char *>(&reqCryptoHeader), sizeof(struct REQ::CRYPTO_HEADER), &key, iv, BF_DECRYPT);

    reqCryptoHeader.login[sizeof(reqCryptoHeader.login) - 1] = 0;

    std::string cryptoLogin(reqCryptoHeader.login);

    if (login != cryptoLogin) {
        state = ST_ERROR;
        respCode = RESP::INVALID_CREDENTIALS;
        message = "Password is invalid";
        printfd(__FILE__, "CONFIG_THREAD::ReceiveData() %s\n", message.c_str());
        return false;
    }

    //assert(reqCryptoHeader.dataSize % 8 == 0);

    char block[1496];
    unsigned char cryptoBlock[1496];
    size_t length = 0;
    uint32_t dataSize = ntohl(reqCryptoHeader.dataSize);

    while (length < dataSize) {
        size_t delta = dataSize - length;
        if (delta > sizeof(cryptoBlock)) {
            delta = sizeof(cryptoBlock);
        }
        size_t bs = delta;
        ReadBlock(cryptoBlock, bs, 5000);
        if (bs != delta) {
            state = ST_ERROR;
            message = "No data within 5 secs";
            printfd(__FILE__, "CONFIG_THREAD::ReceiveData() %s\n", message.c_str());
            return false;
        }

        BF_cbc_encrypt(cryptoBlock, reinterpret_cast<unsigned char *>(block), bs, &key, iv, BF_DECRYPT);

        xml.append(block, bs);

        length += bs;
    }

    return true;
}

void CONFIG_THREAD::Process()
{
    ROOT_PARSER parser(currAdmin, tariffs, users, settings);

    XML_Parser p;

    p= XML_ParserCreate(NULL);
    XML_SetElementHandler(p, &TagBegin, &TagEnd);
    XML_SetUserData(p, &parser);

    if (!XML_Parse(p, xml.c_str(), xml.length(), true)) {
        printfd(__FILE__, "CONFIG_THREAD::Process() Error: '%s' at line %d\n", XML_ErrorString(XML_GetErrorCode(p)), XML_GetCurrentLineNumber(p));
        //MakeErrorXML();
    }

    XML_ParserFree(p);

    xml = parser.GetResult();
}

void CONFIG_THREAD::WriteResp() const
{
    RESP::HEADER respHeader;

    strncpy(respHeader.magic, PROTO_MAGIC, sizeof(respHeader.magic));
    respHeader.version = htonl(2 << 8 | 0);
    respHeader.code = respCode;

    RESP::CRYPTO_HEADER respCryptoHeader;
    strncpy(respCryptoHeader.login, login.c_str(), sizeof(respCryptoHeader.login));
    if (xml.size() % 8 == 0) {
        respCryptoHeader.dataSize = htonl(xml.size());
    } else {
        respCryptoHeader.dataSize = htonl((xml.size() / 8 + 1) * 8);
    }

    size_t size = sizeof(respHeader);
    if (!WriteBlock(&respHeader, size, 5000)) {
        printfd(__FILE__, "CONFIG_THREAD::WriteResp() Failed to send answer header\n");
        return;
    }

    if (state != ST_ERROR) {
        unsigned char buffer[sizeof(respCryptoHeader)];
        size = sizeof(respCryptoHeader);

        BF_cbc_encrypt(reinterpret_cast<unsigned char *>(&respCryptoHeader), buffer, size, &key, iv, BF_ENCRYPT);

        if (!WriteBlock(buffer, size, 5000)) {
            printfd(__FILE__, "CONFIG_THREAD::WriteResp() Failed to send answer crypto-header\n");
            return;
        }

        SendData();
    }
}

void CONFIG_THREAD::SendData() const
{
    size_t pos = 0;
    std::string data(xml);
    if (data.size() % 8) {
        size_t delta = (data.size() / 8 + 1) * 8 - data.size();
        data.append(delta, ' ');
    }
    while (pos < data.size()) {
        unsigned char source[1496];
        unsigned char buffer[1496];

        size_t size;
        if (data.size() - pos > sizeof(source)) {
            memcpy(source, data.c_str() + pos, sizeof(source));
            size = sizeof(source);
        } else {
            memset(source, 0, sizeof(source));
            memcpy(source, data.c_str() + pos, data.size() - pos);
            size = data.size() - pos;
        }

        BF_cbc_encrypt(source, buffer, size, &key, iv, BF_ENCRYPT);

        if (!WriteBlock(buffer, size, 5000)) {
            printfd(__FILE__, "CONFIG_THREAD::SendData() Failed to write data block\n");
            return;
        }

        pos += size; // size?
    }

    return;
}

bool CONFIG_THREAD::CheckLogin(const std::string & login, std::string & password)
{
    currAdmin = admins->FindAdmin(login);

    if (currAdmin == NULL) {
        printfd(__FILE__, "CONFIG_THREAD::CheckLogin() Admin '%s' not found\n", login.c_str());
        return false;
    }

    password = currAdmin->GetPassword();

    return true;
}

void CONFIG_THREAD::TagBegin(void * userData, const char * name, const char ** attr)
{
    ROOT_PARSER * self = static_cast<ROOT_PARSER *>(userData);
    self->StartTag(name, attr);
}

void CONFIG_THREAD::TagEnd(void * userData, const char * name)
{
    ROOT_PARSER * self = static_cast<ROOT_PARSER *>(userData);
    self->EndTag(name);
}