/*
 * Copyright 2009 Funambol, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id$ */

#include <Utils.h>
#include "Logger/LoggerMacroses.h"
#include "serverexchange/WIBConnector.h"
#include "serverexchange/DNSQueryDef.h"
#ifdef PLATFORM_POSIX
#include "serverexchange/DNSQuery.h"
#endif // PLATFORM_POSIX
#include "daemon/ProfileComponentsHolder.h"
#include "NotificationListener/ProcessBootstrapCommand.h"
#include "NotificationListener/BootstrapMessage.h"
#include "executionqueue/IExecutionQueue.h"
#include "DeviceAdapter/IDeviceAdapter.h"

#include "NotificationListener/WSPPushDecoder.h"
using NS_DM_Client::NS_NotificationListener::WSPPushDecoder;

#ifdef PLATFORM_POSIX

#if !defined(PLATFORM_ANDROID)
#include <resolv.h>
#endif

#include <unistd.h>
#include <errno.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#endif // PLATFORM_POSIX

#ifdef PLATFORM_WINDOWS
#include <Windns.h>
#endif

#ifdef PLATFORM_WINDOWS
#include <http/WinTransportAgent.h>
#endif

#include <http/TransportAgentFactory.h>
#include <base/util/ArrayList.h>
#include <base/util/StringBuffer.h>

#include <memory>

#ifdef PLATFORM_ANDROID
int getdomainname (char *name, size_t len)
{
    const char *result = "";    /* Hardcode your domain name if you want.  */
    size_t result_len = strlen (result);

    if (result_len > len)
    {
        errno = EINVAL;
        return -1;
    }
    memcpy (name, result, result_len);
    if (result_len < len)
    name[result_len] = '\0';

    return 0;
}
#endif

using namespace NS_DM_Client;
using namespace NS_DM_Client::NS_Communication;
using namespace NS_DM_Client::NS_DNS_Query;
using namespace NS_DM_Client::NS_NotificationListener;
using namespace Funambol;

static const char * c_LogName = "WIBConnector";
#define LOGGER      NS_Logging::GetLogger(c_LogName)

const char WiMAX_Bootstrap_Service[] = "wimax-bootstrap";
const char LDAP_Service[] = "ldap";
const char WIBDefaultRequestURI[] = "/bootstrap.wib";


WIBConnector::WIBConnector(ProfileComponentsHolder& pch,
                           bool plainBootstrap, bool cipherHeaderPresent,
                           const String& wibServerDiscoveryTarget, const String& wibDnsServer,
                           const String& wibServer, int wibServerPort, const String& wibRequestURI, 
                           int retries, int retryInterval)
: m_retries(retries), m_retryInterval(retryInterval), m_pch(pch), m_pTransportAgent(0),
m_plainBootstrap(plainBootstrap),
m_cipherHeaderPresent(cipherHeaderPresent),
m_wibDiscoveryTarget(wibServerDiscoveryTarget),
m_wibDnsServer(wibDnsServer),
m_wibServer(wibServer),
m_wibServerPort(wibServerPort),
m_wibRequestURI(wibRequestURI)
{
}


WIBConnector::~WIBConnector()
{
    if (m_pTransportAgent)
    {
        delete m_pTransportAgent;
        m_pTransportAgent = NULL;
    }
}


bool WIBConnector::FetchBootstrapData(buffer_t& bsdata, const buffer_t& msid, int retries, int retryInterval,
    const char* domain, const char* service)
{
    bool brc = false;
    if (domain)
    {
        m_domain = domain;
    }
    else
    {
        m_domain.clear();
    }
    m_service = (service) ? service : WiMAX_Bootstrap_Service;
    m_retries = retries;
    m_retryInterval = retryInterval;

    GDLDEBUG("started. Retries=%d, interval=%d, domain='%s', service='%s'", retries, retryInterval, m_domain.c_str(), m_service.c_str());

    do
    {
        ServerList servers;
        if (getWIBServers(servers) && !servers.empty())
        {
            String request;
            String msidStr;
            msidStr.resize(msid.size());
//            memcpy(&msidStr[0], &msid[0], msidStr.length());
            memcpy((void*)msidStr.c_str(), &msid[0], msidStr.length());
            if (buildWIBrequest(msidStr, request))
            {
                String uri;
                buffer_t resp;
                String resp_type;
                EnumWIBQueryResponseCode rc;
                for(ServerList::iterator i = servers.begin(), end = servers.end(); i != end; ++i)
                {
                    if (formatQueryURI(uri, *i, request) && query(uri, rc, resp, resp_type))
                    {
                        GDLDEBUG("Query response code=%d", rc);
                        if (rc == e_WIBQ_Redirect)
                        {
                            GDLWARN("Redirected by '%s'. ", i->server.c_str());
                            // TransportAgent should support automatic redirection.
                            // CurlTransportAgent does.
                        }
                        else if (rc == e_WIBQ_OK)
                        {
                            // Mformation adds WSP header
                            WSPPushDecoder pd;
                            if (pd.SetPDU(&resp[0], resp.size(), true))
                            {
                                NS_WSP::EnumContentTypes ct = pd.GetWNContentType_MIMEType();
                                if (ct == NS_WSP::e_CT_NotWellKnown)
                                {
                                    String ctRef = pd.GetNotWNContentTypeRef();
                                    const String wmfBootstrapCT = "application/vnd.wmf.bootstrap";
                                    char c = 0;
                                    if (ctRef.length() >= wmfBootstrapCT.length()
                                        && strncmp(ctRef.c_str(), wmfBootstrapCT.c_str(), wmfBootstrapCT.length()) == 0
                                        && (c = ctRef.c_str()[wmfBootstrapCT.length()], c == ';' || c == '\0')
                                        )
                                    {
                                        ct = NS_WSP::e_application_vnd_wmf_bootstrap;
                                    }
                                }

                                if (ct == NS_WSP::e_application_vnd_syncml_dm_wbxml
                                    || ct == NS_WSP::e_application_vnd_wmf_bootstrap)
                                {
                                    int bsDataOffset = 0;
                                    int bsDataSize = 0;
                                    if (pd.GetDataInfo(bsDataOffset, bsDataSize)
                                        && checkBootstrapMessage(&resp[0] + bsDataOffset, bsDataSize, bsdata))
                                    {
                                        GDLDEBUG("Bootstrap data decoded. Size=%d", bsdata.size());
                                        m_retries = 0;
                                        brc = true;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    } while (m_retries-- && S_sleep(m_retryInterval));

    // restore retry params
    m_retries = retries;
    m_retryInterval = retryInterval;

    GDLDEBUG("bRes=%d. ", brc);
    return brc;
}


bool    WIBConnector::S_sleep(int interval)
{
    if (interval > 0)
    {
        // sleep
        //return value: Zero if the requested time has elapsed,
        //or the number of seconds left to sleep, if the call was interrupted by a signal handler.
#ifdef PLATFORM_POSIX
        while((interval = sleep(interval)));
#elif defined(PLATFORM_WINDOWS)
        Sleep(interval * 1000);
#endif
    }

    return true;
}

#ifdef PLATFORM_POSIX
bool    WIBConnector::getWIBServers(ServerList& servers)
{
    if (!m_wibServer.empty())
    {
        ServerInfo si;
        si.server = m_wibServer;
        si.port = m_wibServerPort;
        si.weight = 0;
        si.priority = 0;

        servers.push_back(si);
        return true;
    }
    
    String SRVquery;

    bool brc = false;
    const char* domain = (m_domain.empty()) ? NULL : m_domain.c_str();
    if (buildSRVquery(domain, SRVquery) && !SRVquery.empty())
    {
        struct __res_state statbuf;
        statbuf.options = RES_DEFAULT;
        int res = res_init();
        res = res_ninit(&statbuf);
        #define _RES statbuf //#define _RES _res // '_res' is used by libresolv
        if (0 == res)
        {
            do
            {
                if (domain)
                {
                    struct hostent* he = gethostbyname(domain);
                    if (0 == he)
                    {
                        int res = inet_aton(domain, &statbuf.nsaddr.sin_addr);
                        if (0 == res)
                        {
                            GDLERROR("Failed to resolve domain name '%s'. ", domain);
                            break;
                        }
                    }
                    else
                    {
                        memcpy(&statbuf.nsaddr.sin_addr.s_addr, he->h_addr, he->h_length);
                    }

                    statbuf.nsaddr.sin_family = AF_INET;
                    statbuf.nsaddr.sin_port   = htons(NS_DEFAULTPORT);
                }

                dns_answer_t answer;
                answer.resize(4096);
                res = res_nsearch(&statbuf, SRVquery.c_str(), e_CL_IN, e_TY_SRV,
                    &answer[0], answer.size());
                GDLDEBUG("res_nsearch result=%d", res);

                if (res > -1)
                {
                    answer.resize(res);
                    brc = parseSRVresponse(answer, servers);
                    if (!brc)
                    {
                        GDLERROR("Failed to parse SRV Response. ");
                    }
                }
                else
                {
                    int last_err = _RES.res_h_errno;
                    GDLERROR("Resolver error=%d. ", last_err);
                }

            }while(false);
            res_nclose(&statbuf);
        }
        else
        {
            GDLERROR("Resolver initialization failed. res=%d. ", res);
        }

    }
    else
    {
        GDLERROR("Failed to build SRV Query. ");
    }

    return brc;
}

#elif defined(PLATFORM_WINDOWS)
bool    WIBConnector::getWIBServers(ServerList& servers)
{
    if (!m_wibServer.empty())
    {
        ServerInfo si;
        si.server = m_wibServer;
        si.port = m_wibServerPort;
        si.weight = 0;
        si.priority = 0;

        servers.push_back(si);
        return true;
    }

    PDNS_RECORDA dnsRecords = NULL;
    String target;
    target.reserve(128);
    if (m_wibDiscoveryTarget.empty())
    {
        target = "_";
        target += WiMAX_Bootstrap_Service;
        target += "._tcp";

        if (!m_wibDnsServer.empty())
        {
            target += ".";
            target += m_wibDnsServer;
        }
    }
    else
    {
        target = m_wibDiscoveryTarget;
        GDLDEBUG("Using service discovery target='%s'", target.c_str());
    }

    DNS_STATUS status = DnsQuery_A(target.c_str(),
        DNS_TYPE_SRV,
        DNS_QUERY_STANDARD,
        NULL,
        (PDNS_RECORD*)&dnsRecords,
        NULL);

    GDLDEBUG("DNS Query status code=%d, Records=%p, target='%s'", status, dnsRecords, target.c_str());

    PDNS_RECORDA rec = dnsRecords;
    bool brc = false;
    while (rec)
    {
        GDLDEBUG("DNS Record type=%d", rec->wType);

        ServerInfo si;
        if (rec->wType == DNS_TYPE_SRV)
        {
            si.server   = rec->Data.Srv.pNameTarget;
            si.port     = rec->Data.Srv.wPort;
            si.weight   = rec->Data.Srv.wWeight;
            si.priority = rec->Data.Srv.wPriority;

            GDLDEBUG("Server '%s', port %d, priority %d, weight %d. ",
                si.server.c_str(), si.port, si.priority, si.weight);

            servers.push_back(si);

            brc = true;
        }

        rec = rec->pNext;
    }

    if (dnsRecords)
    {
        GDLDEBUG("free DNS records %p", dnsRecords);
        DnsRecordListFree(dnsRecords, DnsFreeRecordList);
    }

    GDLDEBUG("brc=%d", brc);
    return brc;
}
#else
#error "Target platform not defined or not supported"
#endif

#if defined(PLATFORM_WINDOWS)
static int getdomainname(char* buf, size_t size)
{
    *buf = NULL;
    size = 0;
    return 0;
}
#endif

bool    WIBConnector::buildSRVquery(const char* domainname, String& query)
{
    String domain;
    if (!domainname)
    {
        int res = 0;
        int size = 1024;
        const int _MAX_DOMAIN_SIZE = 4096;
        do
        {
            domain.resize(size);
            res = getdomainname(const_cast<char*>(domain.c_str()), domain.size());
            size <<= 1;

          // resize buffer if too small (EINVAL)
        } while (res == EINVAL && size <= _MAX_DOMAIN_SIZE);
        GDLDEBUG("getdomainname result=%d", res);

        if (res != 0 || strlen(domain.c_str()) == 0)
        {
            GDLERROR("Failed to get Domain name (res=%d, domainname='%s'). ", res, domain.c_str());
            return false;
        }
        else
        {
            if (strcmp(domain.c_str(), "(none)") == 0)
            {
                GDLDEBUG("Domain name: '(none)'. Clear. ");
                domain.resize(0);
            }
        }

        domainname = domain.c_str();
    }

    const int domain_len = strlen(domainname);
    query.reserve(100 + domain_len);
    query += "_";
    query += m_service;

    if (domain_len != 0)
    {
        query += "._tcp.";
        GDLDEBUG("Domain name: '%s'. ", domainname);
        query += domainname;
    }
    else
    {
        query += "._tcp";
    }

    GDLDEBUG("DNS Query Server: '%s'. ", query.c_str());
    return true;
}


#ifdef PLATFORM_POSIX
bool    WIBConnector::parseSRVresponse(const dns_answer_t& answ, ServerList& servers)
{
    dns_answer_t::const_pointer  cur = &answ[0];
    size_t max = answ.size();

    const DNS_QUERY_HEADER& dnsQHdr = reinterpret_cast<const DNS_QUERY_HEADER&>(*cur);
    NS_DNS_Query::Header    header(dnsQHdr);

    cur += header.size();
    max -= header.size();

    bool brc = true;
    size_t n = header.questionCount();
    for (size_t i = 0; i < n && max > 0; ++i)
    {
        DnsQuestion q;
        if (q.Set(cur, max, &answ[0], &answ[answ.size() - 1]))
        {
            int length = q.GetLength();
            cur += length;
            max -= length;
        }
        else
        {
            GDLERROR("Invalid Question %d. ", i);
            max = 0;
            brc = false;
        }
    }

    n = header.answerCount();
    for (size_t i = 0; i < n && max > 0; ++i)
    {
        DnsSrvRr rr;
        if (rr.Set(cur, max, &answ[0], &answ[answ.size() - 1]))
        {
            int length = rr.getLength();
            cur += length;
            max -= length;

            ServerInfo  si;
            servers.push_back(si);
            ServerInfo&  ref = servers.back();
            ref.server  = rr.target();
            ref.port    = rr.port();
            ref.priority= rr.priority();
            ref.weight  = rr.weight();
            GDLDEBUG("Server '%s', port %d, priority %d, weight %d. ",
                rr.target().c_str(), rr.port(), rr.priority(), rr.weight());
        }
        else
        {
            GDLERROR("Invalid Answer %d. ", i);
            max = 0;
            brc = false;
        }
    }

    return brc;
}
#endif // PLATFORM_POSIX


static char* char2hex(char c, char* buf)
{
    static const char bits2char[] = "0123456789ABCDEF";
    buf[0] = bits2char [((unsigned char)c) >> 4];
    buf[1] = bits2char [((unsigned char)c) & 0xF];
    buf[2] = 0;
    return buf;
}

bool    WIBConnector::buildWIBrequest(const String& msid, String& request)
{
    String tmp;
    tmp.reserve(1024);

    // "/bootstrap.wib?version=VERSION&msid=MAC&protocol={PROTOCOL}"
    tmp = (m_wibRequestURI.empty()) ? WIBDefaultRequestURI : m_wibRequestURI;

    tmp += "?version=";
    const char VERSION[] = "0";
    tmp += VERSION;

    if (!msid.empty())
    {
        tmp += "&msid=";
        tmp += msid.c_str();

//         const int CHAR2HEX_SIZE = CHAR_BIT / 4;
//         char buf[CHAR2HEX_SIZE + 1];
//         for (size_t i=0, size = msid.size(); i < size; ++i)
//         {
//             tmp += char2hex(msid[i], buf);
//         }
    }

    tmp += "&protocol={";
    const char PROTOCOL_OMA_DM[] = "0";
    tmp += PROTOCOL_OMA_DM;
    tmp += "}";

    bool brc = !tmp.empty();
    if (brc)
    {
        request = tmp;
    }

    GDLDEBUG("brc=%d, request='%s'", brc, tmp.c_str());
    return brc;
}

bool    WIBConnector::query(const String& uri, EnumWIBQueryResponseCode& resp_code, buffer_t& response, String& resp_type)
{
    GDLDEBUG("Query URI: '%s'", uri.c_str());

    Funambol::URL url(uri.c_str());
    if (m_pTransportAgent == NULL)
    {
        // TODO no proxy is used currently
        Funambol::Proxy proxy;
        const size_t readBufferSize = 32*1024;
        m_pTransportAgent = Funambol::TransportAgentFactory::getTransportAgent(url, proxy, DEFAULT_MAX_TIMEOUT, readBufferSize);
        m_pTransportAgent->setReadBufferSize(readBufferSize);
        m_pTransportAgent->setCompression(false);

//         transportAgent->setSSLServerCertificates(config.getSSLServerCertificates());
//         transportAgent->setSSLVerifyServer(config.getSSLVerifyServer());
//         transportAgent->setSSLVerifyHost(config.getSSLVerifyHost());

    }
    else
    {
        m_pTransportAgent->setURL(url);
    }

    if (!m_pTransportAgent)
    {
        GDLERROR("Failed to get TransportAgent");
        return false;
    }

    long result_code = 0;

    m_pTransportAgent->setUserAgent("OMA DM Client");

#ifdef PLATFORM_WINDOWS
    const char propertyAccept[] = "Accept";
    const char propertyAcceptValue[] = "application/vnd.wmf.bootstrap";
    m_pTransportAgent->setProperty(propertyAccept, propertyAcceptValue);
    WinTransportAgent* winTAgent = (WinTransportAgent*)m_pTransportAgent;
    winTAgent->setHttpVerb(HTTP_GET);
    char *responseMsg = m_pTransportAgent->sendMessage("");

    GDLWARN("TODO: get response status code. ");
    if (responseMsg)
    {
        result_code = e_WIBQ_OK;
    }

#else
    StringBuffer accept = "Accept: application/vnd.wmf.bootstrap";
    ArrayList httpHeaders;
    httpHeaders.add(accept);
    char *responseMsg = m_pTransportAgent->query(httpHeaders, &result_code);
#endif // PLATFORM_WINDOWS
    std::auto_ptr<char> responseMsgHolder(responseMsg);
    if(responseMsgHolder.get() == NULL)
    {
        GDLWARN("responseMsgHolder is NULL");
    }

    result_code = m_pTransportAgent->getResponseCode();
    resp_code = (EnumWIBQueryResponseCode)result_code;

    const char* mimeType = m_pTransportAgent->getResponseProperty("Content-Type");
    int resplength = m_pTransportAgent->getResponseSize();

    resp_type = (mimeType) ? mimeType : "";
//  delete [] mimeType;

    response.resize(resplength);
    if (resplength > 0)
    {
        memcpy(&response[0], responseMsg, resplength);
    }

    GDLDEBUG("Response result_code=%d, length=%d, MIME type: '%s'", result_code, resplength, resp_type.c_str());

    return resplength != 0;
}


void    WIBConnector::Start()
{
    buffer_t bsdata;

    buffer_t msid;
    getMSID(msid);

    if (this->FetchBootstrapData(bsdata, msid, m_retries, m_retryInterval))
    {
        ProcessBootstrapCommand* cmd = new(std::nothrow) ProcessBootstrapCommand(bsdata, *m_pch.GetServerExchangeManager());
        if (cmd == (ProcessBootstrapCommand*)NULL)
        {
            GDLERROR("Failed to allocate ProcessBootstrapCommand. ");
        }
        else
        {
            IExecutionQueue* exq = m_pch.GetExecutionQueue();
            if (!exq->Add(*cmd))
            {
                GDLERROR("Failed to add ProcessBootstrapCommand command to Q. ");
            }
        }
    }
}


void    WIBConnector::Stop()
{
    m_retries = 0;
}


bool    WIBConnector::checkBootstrapMessage(const char* buf, size_t size, buffer_t& bsdata) const
{
    BootstrapMessage bm(LOGGER);
    if (!m_plainBootstrap)
    {
        buffer_t emsk;
        size_t emskSize = 256;
        emsk.resize(emskSize);

        if (!m_pch.GetDeviceAdapter()->GetEMSK(&emsk[0], emskSize))
        {
            GDLERROR("failed to get EMSK");
            return false;

        }
        emsk.resize(emskSize);

        bm.SetEMSK(&emsk[0], emsk.size());
        GDLDEBUG("EMSK size=%d. ", emsk.size());

    }

    buffer_t decoded;
    bool brc = bm.Decode(buf, size, m_plainBootstrap, m_cipherHeaderPresent, bsdata);

    GDLDEBUG("Decoding bRes=%d", brc);

    return brc;
}


bool    WIBConnector::formatQueryURI(String& uri, const ServerInfo& i, const String& request) const
{
    uri = "http://";
    uri += i.server;
    if (i.port)
    {
        char tmp[32];
        __sprintf(tmp,":%d", i.port);
        uri += tmp;
    }

    uri += request;

    return !uri.empty();
}


bool WIBConnector::getMSID(buffer_t& msid)
{
    String macAddr;
    if (m_pch.GetDeviceAdapter()->GetDeviceID(macAddr))
    {
#if 0
        if (parseMACAddress(msid, macAddr))
        {
            return true;
        }
#endif // _DEBUG

        msid.resize(macAddr.length());
        memcpy(&msid[0], macAddr.c_str(), msid.size());
        return true;
    }

    return false;
}


bool WIBConnector::parseMACAddress(buffer_t& msid, const String& macAddress)
{
    //
    // if no ':' found use MAC address as it is provided
    //
    if (macAddress.find(":") == String::npos)
    {
        msid.resize(macAddress.length());
        memcpy(&msid[0], macAddress.c_str(), msid.size());
        return true;
    }

    //
    // otherwise (there are ':') parse the MAC address string in format 00:00:00:00:00:00
    // MAC:000000000000 is not supported
    //
    char tail = 0; // should be '\0' after parsing

    const size_t MACAddrSize = 6;
    int b[MACAddrSize] = { -1, -1, -1, -1, -1, -1};

    sscanf(macAddress.c_str(), "%x:%x:%x:%x:%x:%x%c", &b[0], &b[1], &b[2], &b[3], &b[4], &b[5], &tail);

    if ( (unsigned(b[0]|b[1]|b[2]|b[3]|b[4]|b[5]) <= 0xFF) && tail == 0 )
    {
        String tmp;
        char buf[8];
        for (size_t i = 0; i < MACAddrSize; ++i)
        {
            tmp += char2hex((char)b[i], buf);
        }

        msid.clear();
        msid.resize(tmp.length());
        memcpy(&msid[0], tmp.c_str(), msid.size());

        return true;
    }

    //
    // format is not supported
    //
    return false;
}
