/*
 * Copyright (C) 2009 - 2010 Funambol, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
 * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by Funambol" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by Funambol".
 */

/* $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
#include <resolv.h>
#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>

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";


WIBConnector::WIBConnector(ProfileComponentsHolder& pch, 
						   bool plainBootstrap, bool cipherHeaderPresent, 
						   const String& wibServerDiscoveryTarget, const String& wibDnsServer,
						   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)
{
}


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.at(0), &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)
{
    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)
{
    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 += "/bootstrap.wib?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;

#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);

	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 ProcessBootstrapCommand(bsdata, m_pch);
        if (!cmd)
        {
            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;
	macAddr.resize(1024);

	size_t size = macAddr.length();
	if (m_pch.GetDeviceAdapter()->GetDeviceID(&macAddr.at(0), size))
	{
		macAddr.resize(size - 1);
#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
	//
	const char delimiter = ':';
	if (macAddress.find(delimiter) == 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;
}
